Back to blog

IonStack (CVE-2026-10702 + CVE-2026-43499): how a JIT miscompilation and a 15-year-old kernel flaw turned one URL click into full Android 17 root

· · 22 min read
IonStack (CVE-2026-10702 + CVE-2026-43499): how a JIT miscompilation and a 15-year-old kernel flaw turned one URL click into full Android 17 root

On June 24, 2026, YC-backed security startup Nebula Security published a video to X showing a fully updated Android 17 phone handing over complete root access from a single tap on a URL. No app install. No second interaction. The chain runs from an unmodified browser to kernel root in under ten seconds. The exploit, named IonStack, links two zero-day vulnerabilities discovered autonomously by VEGA, Nebula's AI code-scanning agent: CVE-2026-10702, a type-confusion miscompilation in Firefox's IonMonkey JIT compiler, and CVE-2026-43499 (nicknamed GhostLock), a stack use-after-free in the Linux kernel's real-time mutex subsystem that silently shipped in every major Linux distribution for fifteen years. On its own, the Firefox flaw yields remote code execution inside a sandboxed renderer process. On its own, GhostLock requires a local foothold. Chained together, a single URL achieves full device root with no prior access at all. GhostLock carries a CVSS 3.1 score of 7.8 (High) as a standalone local issue; in context, that number badly understates the threat. Both patches are available. The urgency to apply them is real.

This is a two-layered breakdown. Part I gives non-technical readers enough context to decide whether they are exposed and what to do first. Part II walks through the mechanics of the chain the way Nebula reconstructed it. The article closes by examining how Pragma Core addresses exactly the class of problem that made IonStack possible.


Part I. Executive breakdown

What happened

Think of a modern mobile browser as a layered security onion. The innermost layer, called the renderer, is where a web page's JavaScript actually runs. It sits inside a strict sandbox, a kind of padded cell designed to contain any damage from malicious code. Even if an attacker manages to run their own code inside the renderer, they are still walled off from the rest of the phone. A second, separately locked door stands between the renderer and the operating system kernel.

IonStack picked both locks with a single key: a web link.

The first door was opened by a bug in IonMonkey, the Just-in-Time (JIT) compiler inside Firefox's JavaScript engine. JIT compilers speed up JavaScript by translating it to native machine code on the fly, but the translation process involves complex assumptions about data types. When those assumptions break, the compiler can emit code that treats one kind of value as another entirely. CVE-2026-10702 is exactly this kind of flaw: a type-confusion in the compilation step that lets a crafted web page corrupt memory inside the renderer and escape the sandbox entirely. Mozilla patched it in Firefox 151.0.3 on June 2, 2026.

The second door was opened by GhostLock. Once the attacker had code running outside the browser sandbox, they used a fifteen-year-old bug in the Linux kernel's priority-inheritance locking subsystem to climb from an ordinary unprivileged process to full root access. The kernel flaw, CVE-2026-43499, lets any unprivileged local process obtain a dangling pointer into freed kernel stack memory, forge an arbitrary kernel structure over it, and ultimately redirect kernel control flow. Nebula's exploit achieves root in about five seconds with a 97% success rate. It also breaks out of Docker and Kubernetes containers, making shared and cloud-hosted infrastructure equally vulnerable. The kernel patch was merged in April 2026 and backported to stable; Nebula published the full technical writeup on July 7, 2026, alongside open-source proof-of-concept code.

Together the two bugs remove every barrier between a web link and the deepest layer of the operating system.

Who is affected

Component Affected versions Status
Firefox (all platforms) All versions before 151.0.3 Vulnerable. Update to 151.0.3 immediately.
Firefox 151.0.3 and later 151.0.3 and above Patched for CVE-2026-10702.
Linux kernel v2.6.39-rc1 through v7.1-rc1 Vulnerable. Apply distribution kernel update.
Linux kernel 7.1 and later v7.1 and above Patched for CVE-2026-43499.
Android 17 (Firefox installed) All builds with unpatched Firefox Fully vulnerable to the IonStack chain.
Android 14, 15, 16 (Firefox installed) Builds with unpatched Firefox and kernel Potentially exploitable; kernel component present.
Linux desktops and servers Kernels before the April 2026 patch Vulnerable to GhostLock as a standalone local privilege escalation.
Container hosts (Docker, Kubernetes) Unpatched host kernels GhostLock escapes container isolation to host root.

Firefox has over 200 million active users across platforms. Android 17 devices ship on kernels derived from the Linux LTS branches. Whether a given device has the GhostLock patch depends on when its manufacturer pushed the June 2026 security update. At the time of writing, no confirmed in-the-wild exploitation has been reported, but Nebula has open-sourced the full proof-of-concept code, which means the barrier to exploitation is now effectively zero for a capable attacker.

Why this matters beyond Firefox and Android

Browser-to-kernel chains are rare because they require two separately rare things to fail at the same time. What IonStack illustrates is that automated vulnerability research changes the rate at which both halves can be found and combined. VEGA, Nebula's AI scanning agent, discovered both CVE-2026-10702 and CVE-2026-43499 without a human audit, without a conference deadline, and without prior expertise being targeted at either codebase.

That fact is the more important signal here. If a security startup's AI agent can surface a type-confusion in a browser JIT compiler and a fifteen-year-old stack use-after-free in the Linux locking subsystem in the same code sweep, adversaries with access to similar tooling and fewer scruples about disclosure represent a materially higher threat than the pre-2025 landscape assumed.

The GhostLock component extends the risk well beyond mobile. The same kernel ships on Linux servers, cloud virtual machines, container orchestration hosts, and embedded devices. Any team that obtains a foothold through a phishing campaign, a compromised CI runner, or a misconfigured development server can escalate to root in five seconds if the host has not applied the April 2026 kernel patch.

Recommended actions

  1. Update Firefox to version 151.0.3 or later on every device immediately. The update is available now for Windows, macOS, Linux, and Android.
  2. Apply the latest kernel patch from your Linux distribution. Ubuntu, Debian, Red Hat, Fedora, and Alpine have published advisories. Confirm the installed package version matches the patched release; do not assume a pending update has already landed.
  3. Prioritize container hosts, CI runners, and shared Linux servers. GhostLock escapes container boundaries, so an attacker with access to one tenant on a shared host can reach the host kernel. Patch the host kernel before patching the containers.
  4. Inventory every device and server running Firefox, cross-referencing against kernel version. If your organization cannot answer "which machines are still on an unpatched kernel" within one hour, that is a process gap to address regardless of this specific chain.
  5. Enable RANDOMIZE_KSTACK_OFFSET and STATIC_USERMODE_HELPER on Linux builds where recompilation is feasible. These mitigations complicate the specific IonStack exploit path but do not replace the kernel patch.
  6. Monitor for unusual FUTEX_CMP_REQUEUE_PI call sequences and unexpected privilege escalation events. Standard audit logs will not flag GhostLock because it uses ordinary threading system calls, but eBPF-based detection tools such as Falco and Tetragon can be tuned to surface anomalous futex patterns.

Part II. Technical breakdown

IonMonkey, JIT compilation, and the browser sandbox boundary

Firefox's JavaScript engine, SpiderMonkey, runs JavaScript through several tiers of increasing optimization. Code that executes frequently enough eventually reaches IonMonkey (or its successor Warp), which compiles it to native machine code. JIT compilation is a performance-sensitive path: the compiler makes speculative assumptions about value types based on what it has observed at runtime, then emits native code that assumes those types hold. When a value that was always an integer suddenly arrives as an object reference, and the compiler did not guard that path, the mismatch between assumption and reality produces type confusion: native code that reads memory as one type when it actually contains another.

The browser sandbox beneath all of this is a process isolation boundary. The renderer process, where JavaScript runs, has intentionally limited permissions and communicates with the privileged browser process only through a narrow, vetted IPC channel. A successful sandbox escape means the attacker has arbitrary code execution as the renderer process user, but is still constrained by OS process isolation. The kernel below is the next and last barrier.

The vulnerability: CVE-2026-10702, type confusion in IonMonkey

CVE-2026-10702 is a miscompilation in Firefox's JIT that arises from a type-narrowing error in the IonMonkey and Warp backends. The JIT tracks type information through a system of type inference. When it narrows the inferred type of a value at a particular bytecode location, it emits a guard that verifies the type at runtime. If that narrowing is applied incorrectly, subsequent operations execute against mistyped values.

In the affected code path, the compiler incorrectly narrows the type of an object property after a conditional write, concluding that the value at a specific slot is always an integer when it can in fact be an object reference under certain code shapes. The native code that follows reads the slot as an unboxed integer, performing arithmetic on what is actually a pointer. The immediate result is controlled memory corruption inside the renderer heap.

// Simplified shape that triggers the miscompilation
function trigger(arr, flag) {
  if (flag) {
    arr[0] = {};       // slot 0 is written an object reference
  } else {
    arr[0] = 42;       // slot 0 is written an integer
  }
  // IonMonkey incorrectly narrows arr[0] as integer below
  // when flag=true, it reads the object pointer as int32
  return arr[0] + 1;
}
// Repeated calls with flag=false train the JIT type profile.
// A call with flag=true then triggers the type confusion.

Controlled corruption of the renderer heap, when aimed at specific SpiderMonkey internal structures such as typed array backing buffers or the JIT code cache, develops into full read-write access to the renderer's address space. The precise sandbox escape technique Nebula used in IonStack has not been fully published. The focus of the disclosure is the kernel half.

Mozilla patched CVE-2026-10702 in Firefox 151.0.3 (MFSA 2026-54, June 2, 2026). The fix corrects the type narrowing logic so that it does not conclude a slot is definitively integer-typed when a code path exists that writes an object reference to it.

GhostLock: root cause of CVE-2026-43499

The kernel half of IonStack lives in kernel/locking/rtmutex.c, specifically in the function remove_waiter(). Understanding the bug requires brief background on the priority-inheritance futex mechanism.

Linux supports PI futexes: user-space futex words paired with kernel-managed real-time mutex objects. When a high-priority task blocks waiting for a PI futex held by a lower-priority task, the kernel temporarily boosts the holder's priority (priority inheritance). This involves waiter objects allocated on the blocking task's own kernel stack.

FUTEX_CMP_REQUEUE_PI allows one thread to move another's waiter from one futex queue to a different PI futex. Internally, rt_mutex_start_proxy_lock() enqueues the waiter on behalf of the sleeping thread (the "waiter task"), while the thread calling FUTEX_CMP_REQUEUE_PI (the "requeuer") drives the operation. When the proxy lock attempt encounters a deadlock, it rolls back by calling remove_waiter().

The bug is a case of a function reused by a caller it was never designed for.

remove_waiter() was originally written for the self-blocking path, where the thread cleaning up is the same thread that owns the waiter. It always cleared current->pi_blocked_on, where current is whichever thread is executing at that moment.

On the FUTEX_CMP_REQUEUE_PI rollback path, current is the requeuer, not the waiter task. The waiter task's stack-allocated rt_mutex_waiter still has pi_blocked_on pointing at itself. When the waiter task eventually returns from its futex syscall, that stack frame is gone and the pointer is dangling.

// kernel/locking/rtmutex.c (vulnerable version)
static void __sched remove_waiter(struct rt_mutex_base *lock,
                                  struct rt_mutex_waiter *waiter)
{
    ...
    raw_spin_lock(&current->pi_lock);
    rt_mutex_dequeue(lock, waiter);
    current->pi_blocked_on = NULL;  // BUG: clears requeuer, not waiter task
    raw_spin_unlock(&current->pi_lock);
    ...
    rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock,
                               next_lock, NULL, current); // BUG: walks from requeuer
}

The fix, committed in April 2026 as 3bfdc63936dd, reads waiter->task instead of current:

-  raw_spin_lock(&current->pi_lock);
-  rt_mutex_dequeue(lock, waiter);
-  current->pi_blocked_on = NULL;
-  raw_spin_unlock(&current->pi_lock);
+  scoped_guard(raw_spinlock, &waiter_task->pi_lock) {
+    rt_mutex_dequeue(lock, waiter);
+    waiter_task->pi_blocked_on = NULL;
+  }
...
-           next_lock, NULL, current);
+           next_lock, NULL, waiter_task);

The bug evades lockdep entirely because lockdep only checks that some pi_lock is held, not whose.

Triggering the stack UAF and turning it into root

To trigger the deadlock rollback, Nebula constructs a PI dependency cycle with three futex words and three threads: a waiter thread that holds f_pi_chain and blocks on f_wait; an owner thread that holds f_pi_target and blocks behind the waiter; and a requeuer that calls FUTEX_CMP_REQUEUE_PI to move the waiter onto f_pi_target. The kernel's chain walk closes the cycle and returns -EDEADLK, triggering the buggy rollback. The waiter task returns to userspace with pi_blocked_on pointing at its own freed stack frame. The UAF window is now open indefinitely.

The exploit proceeds through five stages:

Stage 1: KASLR bypass via prefetch timing. An unprivileged process times prefetch instructions across the kernel virtual address space. Because Linux randomizes the kernel image base with only about 9 bits of entropy, a small number of timing samples recovers the KASLR slide with near-100% reliability. A second round of the same technique recovers the physmap base, giving the attacker both the direct-map address of the CPU entry area (CEA) and the address of any kernel symbol.

Stage 2: Stack reclaim. The waiter thread returns from the futex syscall and immediately calls prctl(PR_SET_MM, PR_SET_MM_MAP, ...). Inside the kernel, prctl_set_mm_map() copies a user-controlled auxv array into a fixed-size stack buffer at roughly the same stack depth as the freed rt_mutex_waiter. Controlled bytes land back on top of the old object, forging a fake rt_mutex_waiter with attacker-chosen fields.

Stage 3: Constrained write. A separate thread calls sched_setattr() on the waiter task to trigger a PI chain walk. The kernel follows the dangling pi_blocked_on pointer into the forged waiter, performs an rb-tree erase on the fake lock object, and writes the child pointer of the fake tree node into the root slot of inet6_protos[IPPROTO_UDP], a pointer inside a writable kernel function table whose adjacent slots happen to satisfy the spinlock-unlocked and owner-clear constraints the erase requires.

// Layout exploitation targets in inet6_protos[]
inet6_protos[16]  == NULL           // fake wait_lock: reads as unlocked
inet6_protos[17]  == &udpv6_protocol // TARGET: IPPROTO_UDP handler pointer
inet6_protos[18]  == NULL           // fake rb_leftmost
inet6_protos[19]  == NULL           // fake owner

Stage 4: Control flow hijack. With inet6_protos[IPPROTO_UDP] redirected into the CPU entry area, Nebula sprays a fake inet6_protocol structure there and triggers a loopback IPv6 UDP packet. The kernel dereferences the overwritten handler pointer, handing the attacker a program counter. A short JOP chain hosted in the same CEA window pivots the kernel stack.

Stage 5: DirtyMode and root. Rather than a full return-to-user or long ROP chain, Nebula uses a single write to flip the mode bits of the core_pattern sysctl table entry to world-writable. An unprivileged process then writes a core-dump helper path into /proc/sys/kernel/core_pattern and crashes a helper process to execute an arbitrary binary as root. The entire sequence runs in approximately five seconds.

No special kernel configuration is required. The only prerequisite is CONFIG_FUTEX_PI=y, which is enabled by default in every distribution Nebula tested.

Affected versions

The IonStack chain requires both components:

GhostLock alone also affects Linux desktops, servers, and containers. Container hosts running unpatched kernels are vulnerable to local privilege escalation and container escape regardless of Firefox.

Timeline

Date Event
2011 (approx.) Linux 2.6.39 ships, introducing the flawed remove_waiter() logic. GhostLock is born.
Unknown IonMonkey JIT type-narrowing miscompilation introduced. Precise commit not yet public.
2026-04-18 Nebula reports CVE-2026-43499 and a draft patch to [email protected].
2026-04-20 Kernel fix committed upstream as 3bfdc63936dd.
2026-05-04 Fix backported to stable kernel branches.
2026-06-02 Mozilla releases Firefox 151.0.3, patching CVE-2026-10702 (MFSA 2026-54).
2026-06-24 Nebula publicly demos IonStack on X: one URL click roots an Android 17 device.
2026-06-30 Google kernelCTF acknowledges the GhostLock submission and awards $92,337.
2026-07-07 Nebula publishes the full GhostLock technical writeup and open-source PoC.
2026-07-08 Major security press coverage; PoC code publicly available.

A note on the discovery methodology

Both vulnerabilities were found by VEGA, Nebula's AI code-scanning agent, without human auditors targeting either codebase. For GhostLock, an automated system analyzed the locking subsystem, identified the assumption mismatch in remove_waiter(), constructed a proof that a requeuer's pi_blocked_on could remain dangling after a rollback, and verified the primitive experimentally. For the Firefox flaw, the same system audited the IonMonkey type inference tables and flagged the narrowing path that produces type confusion under a specific code shape.

Two observations follow. First, both bugs had evaded human audit and existing tooling for over a decade. The fifteen-year age of GhostLock reflects how difficult the kernel locking subsystem is to audit manually, not exceptional negligence. An automated system that reasons about data flow and ownership assumptions across function call boundaries can cover that space in ways that manual review cannot sustain at scale. Second, the combination itself is what elevates IonStack above either component alone. A browser JIT bug in isolation is a renderer exploit. A kernel LPE in isolation requires a local foothold. Neither would have been paired as quickly by teams auditing each codebase separately. VEGA found both in the same sweep and, by implication, paired them.

The security community should read that as a signal: the time between vulnerability discovery and working chain is compressing, and the compression is driven by automation rather than by any particular researcher's expertise.


What we should learn from IonStack

  1. CVSS scores describe components, not chains. GhostLock's 7.8 rating reflects a local-only vulnerability. Combined with CVE-2026-10702, the effective severity is remote critical. Every privilege escalation bug should be triaged against the realistic foothold scenarios available to an attacker, not just its individual score in isolation.

  2. Fifteen years is a normal age for a kernel bug. GhostLock was introduced in 2011 in a helper function that was later extended to a new caller without updating its assumptions about which task current represented. Kernel code in locking, scheduling, and IPC subsystems accumulates this class of latent bug across long maintenance lifetimes. AI-assisted research applied to these mature codebases will continue to surface vulnerabilities of similar age.

  3. AI-native vulnerability research changes the threat model. IonStack required deep simultaneous expertise in JIT compiler internals and kernel locking semantics, across two distinct codebases, and it was assembled without a human directing any search at either target. Security programs that calibrate risk on the assumption that chains like this require rare, expensive, manually-directed research need to revise that model now.

  4. Stack UAFs in kernel code warrant specific detection investment. GhostLock is a use-after-free on the kernel stack, a class distinct from heap UAFs in its exploitation mechanics: the freed frame is quickly reclaimed by a subsequent syscall on the same thread, producing a wide and stable UAF window. Static analysis can be tuned to flag functions that clear current->pi_blocked_on or similar task-pointer fields and are reachable from a proxy or delegate call path where current differs from the task being acted on. This pattern is not unique to GhostLock.

  5. Container boundaries do not substitute for host kernel hygiene. GhostLock escapes containers. Multi-tenant environments, CI/CD pipelines, and shared development infrastructure that rely on container isolation as a security boundary, without maintaining host kernel patch currency, are exposed. Defense in depth means both container isolation and a patched host kernel, not either one alone.


How Pragma Core addresses this class of problem

Pragma Core is built for vulnerabilities of exactly this shape: bugs that are invisible to single-function scanners because the flaw lives in the relationship between callers and callees, or in the interaction between two separate subsystems. Both halves of IonStack fall into this category. The Firefox bug arose from a narrowing assumption that was correct at one call site and wrong at a different one. The kernel bug arose from a cleanup function that was correct in its original context and wrong when reused by a new caller. A scanner that checks individual functions finds neither. A system that traces conditions across call chains finds both.

Autonomous AI agents for full-chain investigation

Pragma Core's autonomous agents reason over attack chains rather than stopping at the line that triggers a crash. Posed the question "can an unprivileged caller reach remove_waiter() on the Requeue-PI path where current differs from waiter->task?", the agent would walk the call graph from FUTEX_CMP_REQUEUE_PI through futex_proxy_trylock_atomic through rt_mutex_start_proxy_lock, confirm the proxy path exists, and flag the assumption mismatch in remove_waiter(). The same agent, working on the SpiderMonkey codebase, would trace type narrowing through the IonMonkey optimization pipeline and identify narrowing conclusions that do not account for all write paths to the same property slot. These are precisely the questions that took fifteen years and a public chain exploit to surface in the wild.

Interactive call graphs with vulnerability overlay

Pragma Core auto-generates call graphs for any connected repository and overlays findings on them, so teams see where untrusted input propagates and where trust boundaries are crossed. For GhostLock, the graph would surface the fact that remove_waiter() is called from two distinct code paths with different semantics about which task current represents: the self-blocking path and the proxy-lock rollback path. The finding would sit on the edge between rt_mutex_start_proxy_lock and remove_waiter, annotated with the ownership condition that breaks in the second context. No engineer auditing remove_waiter() in isolation would see it; the call graph makes it a first-class visible relationship between two callers and one shared assumption.

SAST tuned for proxy-caller assumption mismatches

Standard SAST is tuned for injection sinks and heap allocation mistakes. The pattern behind GhostLock is more subtle: a function that assumes current is the task being acted on, reused from a call site where that assumption does not hold. Pragma Core's static analysis can be configured to flag functions that dereference current in a locking or ownership context and are invoked from a proxy, delegate, or requeue path where current is a third party to the operation. This pattern recurs across kernel subsystems that accumulate new callers without revisiting the original function's assumptions. Applying the rule broadly, rather than only after a chain like IonStack surfaces it publicly, is what converts a detection into a proactive finding.

Full SBOM with component version visibility across the fleet

One of the most concrete gaps exposed by IonStack is the "which of our machines are still vulnerable?" problem. Most organizations could not answer that question for Firefox versions and kernel patch levels in the same hour. Pragma Core generates a complete component inventory per repository and deployment, including kernel versions tracked through infrastructure-as-code definitions and browser versions tracked through fleet management integrations. The moment CVE-2026-43499 entered Pragma Core's CVE catalog, every team with a connected Linux environment would receive a ranked list of which machines needed patching and in what priority order, without manual inventory queries.

Continuous AI security research, not point-in-time audits

The GhostLock kernel fix was committed in April 2026. The public writeup and PoC arrived in July. Any organization running Pragma Core's continuous research against their Linux-derived infrastructure would have had access to the kernel-level analysis during that gap window, not just after the demo video ran. The same lead time applies to the Firefox JIT: continuous research against the open-source SpiderMonkey codebase would have flagged the type narrowing candidate for investigation weeks before the chain was demonstrated publicly.

Human-guided AppSec investigations for the hard cases

GhostLock is the kind of bug where a generalist scanner produces silence and an expert kernel researcher produces a root exploit. Pragma Core's human-guided research module lets an AppSec operator direct agents at a specific subsystem, for example "enumerate all callers of remove_waiter() and check whether the caller's identity of current matches the intended task in each context," supported by the platform's existing code context and call graph. This is precisely the investigation workflow that found GhostLock, applied systematically to an organization's own codebases before a startup's AI agent surfaces it publicly first.


Closing thoughts

IonStack is not, at its core, a bug about Firefox. It is not a bug about Android. It is not even a bug about a fifteen-year-old kernel. It is a bug about assumption reuse: the pattern where a correct invariant in one context becomes a silent error when a function is extended to a new caller, across any software layer, in any language, at any depth in the stack.

remove_waiter() was correct in 2011. CVE-2026-10702's narrowing logic was correct for the code shapes it was originally tested against. Neither author made an obvious mistake at the time. Both mistakes became exploitable when the surrounding code grew to include a path that violated the original assumption without anyone auditing the original function for new callers.

This pattern is pervasive in mature codebases, and it is largely invisible to tools that check functions one at a time. The difference between finding it proactively and reading about it in a demo video is code visibility, the right analytical questions, and the automation to ask them continuously.

Organizations that want to move from "we scan and report" to "we systematically investigate what is fragile" can explore what Pragma Core makes possible at pragma-core.com.


Sources

Related posts
CVE-2026-74820: how an unsanitized ORDER BY clause turned ServiceNow's AI Platform into an unauthenticated database backdoor
Sep 18, 2026
CVE-2026-85978: how one path normalization mismatch turned Akana's admin console into unauthenticated remote code execution
Sep 9, 2026
CVE-2026-78174: how an unredacted session token in a diagnostic log turned a low-privileged WatchGuard Dimension admin into super admin
Sep 1, 2026

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →