← Research

OEMpocalypse Now: A Generic Exploitation Strategy from Android untrusted app to root

Part 1 of a series that takes an unprivileged Android app to root on Samsung, Xiaomi, and Oppo/OnePlus/Realme devices, with a single strategy.

Lukas Maar

August 31, 2026

OEMpocalypse Now

On Android, every third-party app runs in a sandboxed context called untrusted_app. If you ask five offensive security researchers how to go from this context to root, you will most likely get five different strategies, and each has its own trade-offs. Here, I describe the one I took and why, measured against three properties I use as a yardstick throughout:

  1. Reliability, i.e., succeed at near-100 percent regardless of which defenses or customizations are enabled.
  2. Portability, i.e., run with minimal per-kernel-version, per-OEM, per-chipset, or per-device adjustment.
  3. Universality, i.e., cover as many devices as possible.

The core idea is to only target code written by Original Equipment Manufacturers (OEMs) such as Samsung and Xiaomi. Specifically, I target a page Use-After-Free (UAF) in an OEM-specific kernel driver, using an OEM-specific sandbox escape to reach that driver where the OEM's SELinux policy requires one. In brief, a page UAF is a bug that leaves a live reference to a physical memory page after the kernel has freed it, and a sandbox escape is any bug that lets code cross from a lower-privilege process into a higher-privilege one. I then instantiated the strategy three times, once per major Android OEM, finding multiple vulnerabilities along the way and yielding chains that cover all Samsung flagship devices (at least the Galaxy S23 through S26 series and the recent Z series), a large share of Xiaomi mid-range to flagship devices, and recent Oppo, OnePlus, and Realme flagship devices.

Figure 1: The Samsung chain end to end on a bootloader-locked Galaxy S26 Ultra. Recordings for the other four tested devices are in The Three Chains at a Glance.

This first post explains the reasoning behind the strategy and compares it with two alternatives. The subsequent posts will provide technical details of these chains.

TL;DR

  • Strategy: Exploit a page UAF in an OEM-specific kernel driver. Where the OEM's SELinux policy gates that driver behind a privileged domain, reach it via an OEM-specific sandbox escape first. Every bug involved lives in OEM code rather than in generic Linux or chipset drivers.
  • Why: A page UAF reached from the right context can provide a stable physical page-level primitive that is largely independent of kernel version, slab hardening, KASLR, CFI, and the specific phone model, which is exactly what a weaponized chain needs.
  • Coverage: Three instantiations of the same strategy cover all Samsung flagship devices (at least the Galaxy S23 through S26 series and the recent Z series), most Xiaomi mid-range to flagship devices, and recent Oppo, OnePlus, and Realme flagships.

Overview

The Android Kernel Attack Surface

Before getting to OEM-specific code, it is worth stepping back and looking at what an unprivileged app can actually reach in the kernel, and what each kind of kernel bug offers for a weaponized exploit.

The list is short due to three layered mechanisms: standard Unix Discretionary Access Control (DAC) via Android's per-app UID model, SELinux as the Mandatory Access Control (MAC) layer on top, and system call filtering via seccomp. Every third-party app on Android runs under its own UID, in the untrusted_app SELinux domain (or one of its per-API-level variants such as untrusted_app_32), and behind a seccomp filter. DAC gates access via file ownership and mode bits, the SELinux policy restricts which device nodes, sockets, and filesystem paths the domain may access, and seccomp prevents the app from invoking a subset of system calls. The combined effect is that most of the kernel, and in particular most of the driver stack, is simply not reachable without first moving to a more privileged domain. In other words, DAC, SELinux, and seccomp together define the kernel attack surface you get to start from.

To make this concrete, let's take a current flagship line, e.g., the Samsung Galaxy S26 family, and enumerate the kernel attack surface from an untrusted_app process. On these devices, the list is roughly the following:

  • There is the core Linux syscall surface that survives seccomp, e.g., memory management, the VFS layer, futexes, timers, basic networking, and Binder via /dev/binder.
  • There are the shared-memory and DMA buffer (DMA-BUF) heap nodes that the graphics and other media stacks depend on.
  • There is the GPU driver, exposed as /dev/kgsl-3d0 on the Snapdragon models (e.g., the S26 Ultra) and via the DRM render node on the Exynos/Xclipse models (e.g., the base S26 in some regions), because apps need direct GPU access for rendering.
  • And then there is a tail of nodes that are less universal, e.g., DSP or NPU interfaces on some chipsets, plus a handful of OEM-specific additions whose presence depends entirely on what the OEM's own SELinux policy chose to expose.

I should state that this enumeration is not meant to be exhaustive or authoritative. It is what I found on the devices in front of me, and the exact set may shift between models and Android releases.

Looking at that list, the entries loosely group by who wrote the code behind them: upstream Linux and Android Common Kernel code, chipset drivers, and OEM-specific code.

Before walking through these groups, let me state what we are actually optimizing for. In my view, an ideal weaponized untrusted_app-to-root exploit has three properties. First, it is reliable, i.e., it succeeds at near-100 percent regardless of which defenses and customizations a given OEM or kernel version happens to enable. Second, it is portable, i.e., it runs with as few per-kernel-version, per-OEM, per-chipset, or per-device adjustments as possible. Third, it is universal, i.e., a single exploit covers as many devices as possible. Put together, this is the "one exploit to rule them all" ideal. Most likely no real exploit fully reaches it, but it is the bar I measure candidates against. With that bar in mind, generic Linux bugs are the natural place to start, since on paper they are the most universal thing on the list.

Generic Linux Kernel Vulnerabilities

The first group is the code that every Android device shares: the upstream Linux kernel and the Android Common Kernel (ACK) additions on top of it. Bugs here, e.g., in io_uring, the network stack, or the memory-management core, are attractive precisely because they are OEM- and chipset-agnostic. A single exploit should, in principle, work on a Samsung, a Pixel, and a Xiaomi running the same kernel branch. IonStack is a good example of this line of work, and I think it is genuinely impressive: a single memory-management bug in generic code, pushed all the way to root across devices from multiple OEMs. It shows how much reach a well-chosen generic bug can have.

In practice, that reach is also where most of the engineering effort goes, for two reasons. The first reason is the typical nature of the bugs. The accessible core Linux code has seen years of external auditing, and the resulting primitive from what remains exploitable is usually constrained and lives at the slab level, e.g., a UAF of one specific object type, a limited Out-Of-Bounds (OOB) write, or a race window that has to be won, rather than direct control over a physical page. Turning that into root typically means heap grooming, cross-cache tricks, an information leak, and other exploit techniques. All of those can be sensitive to the target's system. The second reason is that a single exploit has to stay stable across three axes of variation at once. There is the kernel version, since OEMs ship anything from 5.15 to 6.12 on modern devices. There is the OEM layer, since OEMs may apply their own patches to the kernel source (e.g., Samsung's RKP and KDP) and may ship their own runtime configuration (e.g., whether mem-cg is enabled). And there is the individual device, since even two phones from the same OEM on the same kernel branch ship different hardware, e.g., RAM size and core count, which shifts allocator behavior such as per-CPU page list watermarks and the number of per-CPU slab caches to account for. Each axis can add another dimension of per-target work, where the fully weaponized exploit gradually turns into a matrix of special cases that needs ongoing attention as monthly bulletins land.

Put together, the two reasons point in the same direction: the bug itself is present almost everywhere, but turning it into a reliable and portable exploit is where the work concentrates. With that picture in mind, let's look at the next group.

Chipset Drivers Reachable from untrusted_app

The second group is drivers for chipset components that are, for performance reasons, exposed directly to unprivileged apps, such as the GPU (Mali, Adreno KGSL, PowerVR) and the Digital Signal Processor (DSP) or Neural Processing Unit (NPU). This is where a large and highly visible share of the public Android Local Privilege Escalation (LPE) work of the last few years lives, both the research writeups and the exploits caught in the wild. To give a few examples on the in-the-wild side, Mali GPU bugs have been caught repeatedly, and the Qualcomm DSP driver adsprpc has shown up in several chains. Examples on the research side include Seth Jenkins at Project Zero, who has written up the DSP and other reachable drivers in detail, and Man Yue Mo at GitHub Security Lab, who has a long series of Mali and Adreno GPU exploitation posts.

These bugs are genuinely great to work with. The drivers manage physical pages and GPU or DSP page tables on behalf of userspace, so a bug often hands you a page UAF or an arbitrary physical mapping more or less directly, with little of the heap grooming, cross-cache work, or leak hunting a generic Linux bug typically requires. The reason is structural: to let a GPU or DSP operate on user data, the driver has to pin pages, map them into the device's own page table, and later tear that mapping down and release the pages. Any lifetime mistake along that path, e.g., a page released while a device mapping still points at it, or a reference dropped once too often, is a page-level primitive more or less by construction. That is a very different starting point from a slab-level UAF in core Linux code.

The primitive, then, is typically the more straightforward side of these bugs. The breadth of device coverage afforded by a single bug is a separate question. It depends on the chipset the bug affects, and it varies widely. At one end, a bug in shared Qualcomm code such as adsprpc or KGSL can span many Snapdragon generations and, because Snapdragon sits in a large share of the flagship market, reach devices across several OEMs at once. At the other end, a bug tied to one particular Mali or Exynos IP block, or to one MediaTek driver revision, covers a much smaller slice. It is also worth noting that the chipset partition does not line up with the OEM partition: a single OEM often ships more than one chipset family across its lineup, e.g., Samsung sells Exynos flagships in some regions and Snapdragon flagships in others, so one chipset bug rarely maps onto one OEM's full lineup or vice versa.

Both of the groups we have looked at so far have one thing in common: they are reachable directly from untrusted_app, and that surface has by now been mapped and audited fairly systematically. That leaves the third group on our list, OEM-specific code, most of which sits behind more privileged domains and may have received noticeably less of that attention. I was curious about what it offers, so rather than argue here that it is better or worse than the alternatives, let's simply walk through what it looks like.

The OEMpocalypse Strategy

By OEM-specific code I mean the kernel drivers and userspace services that the OEM, i.e., Samsung, Xiaomi, or Oppo, ships as part of its own software stack on top of Android. Crucially, this code is tied to the OEM's software layer (One UI, HyperOS, ColorOS) rather than to the chipset underneath. So, the same component tends to be present across that OEM's lineup, whether a given device has a Snapdragon, an Exynos, a Dimensity, or anything else inside. That is the coverage property I was looking for: one bug per OEM that, in principle, reaches every model shipping the affected component.

The strategy, then, is to find a page UAF in one of these OEM drivers. Ideally that is a single-stage attack, i.e., the driver is reachable from untrusted_app directly, and we resort to an initial stage only where the OEM's SELinux policy forces us to.

Stage 1: Escaping the Sandbox

The underlying reason sandbox escapes exist at all is that isolation on Android is not absolute. Apps, system services, and native daemons are separated into their own UIDs and SELinux domains, but they still need to talk to each other, and Android gives them plenty of ways to do so: Binder calls, intents, content providers, local sockets, and so on. Every one of these Inter-Process Communication (IPC) endpoints is a place where a less-privileged caller sends input to a more-privileged callee and trusts that callee to enforce the boundary correctly. OEMs then add a large number of their own apps, services, and daemons on top of AOSP, each with its own set of such endpoints. The consequence is that a single logic flaw in one of these OEM IPC handlers, e.g., a missing caller check, an exported component that should not be, or an unvalidated file path, is enough to cross a privilege boundary. Crucially for our goals, this kind of bug lives entirely in OEM userspace logic. It does not depend on the chipset, on the kernel version, or on the specific phone model, because the same OEM service with the same flawed check ships on every device running that OEM's software layer.

So stage 1, where it is needed, is exactly that: use a logic flaw in an OEM IPC endpoint to move from untrusted_app into an OEM-owned process, e.g., a system app, a platform-signed service, or a native daemon, whose SELinux domain is allowed to open the kernel driver we want to reach in stage 2. I will go into the specific escapes in the per-OEM posts. The short version is that anything from a confused-deputy primitive up to full native code execution in the target process works here, as long as it can drive the stage 2 driver's interface.

Stage 2: The Page UAF

The second stage is a page UAF in an OEM kernel driver reachable from the domain we now hold, whether that is untrusted_app itself or the domain reached via stage 1. The reason I specifically look for a page UAF, rather than any other bug class, is that the resulting primitive is largely independent of everything that normally makes Android kernel exploitation target-specific.

Concretely, a page UAF gives us a reference that keeps pointing at a physical page after the kernel has freed and reallocated it. Depending on the driver, that reference can be a CPU-side userspace VMA, a device-side mapping such as an IOMMU page-table entry, or a driver-held struct page pointer that ends up on a DMA path. Either way, the effect is the same. We arrange for the freed page to be reused as kernel memory of our choosing, e.g., a page-table page or a slab page holding structures we can turn into an arbitrary read/write primitive, and then read and write it through the stale mapping. The key point is how much this primitive reduces dependence on the usual exploitation machinery. It operates at the page level, so slab-focused hardening such as CONFIG_SLAB_BUCKETS, CONFIG_RANDOM_KMALLOC_CACHES, or CONFIG_SLAB_FREELIST_HARDENED does not constrain the stale mapping. It typically does not need a KASLR leak, because we read and write through a mapping we already hold rather than dereferencing a kernel pointer. And it is data-only, so control-flow protections such as CFI do not constrain it. Finally, the page allocator's behavior is comparatively stable across kernel versions. In these chains, the same reclamation and page-fengshui code ran unchanged from 5.15 through 6.12.

The Trade-off

The first limitation is coverage. The strategy gives up on being cross-OEM, so a Samsung chain does nothing on a Xiaomi device and covering three OEMs means three independent chains. And even within one OEM, not every component may ship on every device: Samsung, for example, enables different tiers of its software stack depending on whether a device is a flagship or a lower/mid-end model, so a component that is present across the Galaxy S and Z lines may be absent on an A-series phone. In practice the coverage of each chain is exactly the set of devices the OEM chose to ship the vulnerable component on.

The second limitation is the number of moving parts a weaponized attack has to track. Each two-stage chain depends on at least two independent bugs, and across OEMs that is a handful of vulnerabilities that all have to remain unpatched at the same time. When any one of them is fixed, that chain needs a replacement bug. Keeping the set alive is real ongoing work.

What makes the trade-off attractive to me despite those costs is what you get in return. Wherever the OEM does ship the vulnerable components, it ships the same code regardless of the hardware underneath, so within that set of devices the chain is reliable and portable: a chain that works on one Samsung flagship works on the rest with a much smaller per-device adjustment matrix. Three chains built this way cover a large fraction of the Android flagship market. Measured against the three properties from earlier: reliable and portable are close to the ideal, while universal is scoped down from "all Android" to "most devices from one OEM".

The Three Chains at a Glance

Here is what the three chains look like from the outside. Each one is packaged as an ordinary Android app with no declared permissions. Each demo video first shows the device's Android and kernel version, then prints the app's own context to show it is running as untrusted_app with no additional permissions. The app then runs the exploit, drops into a root shell, and prints the same block again:

User:            uid=0(root) gid=0(root) groups=0(root) context=u:r:kernel:s0
SELinux:         Permissive
Verified boot:   green
Bootloader lock: 1
VBMeta state:    locked
Kernel:          ...
Nodename:        ...
Build:           ...

The Verified boot: green, Bootloader lock: 1, and VBMeta state: locked lines are the important ones: they confirm that the device is running a stock, signed image with a locked bootloader.

(a) Samsung chain, Samsung Galaxy S26.
(b) Xiaomi chain, Xiaomi 17.
(c) Oppo-family chain, Oppo Find X9 Ultra.
(d) Oppo-family chain, OnePlus Ace 6 Ultra.

Figure 2: The three chains running end to end on bootloader-locked stock devices.

The recordings above show the Samsung chain on a Galaxy S26, the Xiaomi chain on a Xiaomi 17, and the Oppo-family chain on an Oppo Find X9 Ultra and a OnePlus Ace 6 Ultra, all on the latest firmware available as of early August 2026. The Galaxy S23 is not shown: I used it as a rooted development device to build the Samsung chain with kernel visibility, and then ported the result blindly to the locked S26 and S26 Ultra. The table below summarises the devices I tested each chain on.

OEMChainDeviceChipsetAndroidKernelFirmware
SamsungPart 2Galaxy S26 UltraSnapdragon 8 Elite Gen 5166.12.30Jul 2026
Galaxy S26Exynos 2600166.12.38Jul 2026
Galaxy S23Snapdragon 8 Gen 2145.15.148Mar 2025
XiaomiPart 3Xiaomi 17Snapdragon 8 Elite Gen 5166.12.23Jul 2026
Oppo familyPart 4Oppo Find X9 UltraSnapdragon 8 Elite Gen 5166.12.58Jul 2026
OnePlus Ace 6 UltraMediaTek Dimensity 9500166.12.58Jul 2026

Conclusion

This post set out a yardstick for weaponized untrusted_app-to-root chains, i.e., reliable, portable, and universal, and walked through the three groups of kernel code behind it: generic Linux, chipset drivers, and OEM code. Generic Linux and chipset-driver bugs are both well-understood routes with real strengths. The third group is OEM-specific code and the OEMpocalypse strategy is my attempt to explore it: find a page UAF in an OEM kernel driver, and, where the OEM's SELinux policy requires it, reach that driver via an OEM sandbox escape first. Instantiated once per major OEM, that gave three chains covering a large slice of the current Android flagship market.

On the defensive side, my takeaway is that a page UAF is an inherently hard bug class to mitigate once one is present in kernel code. The hardening that has landed in Android kernels over the last few years, e.g., slab freelist and bucket protections, KASLR, CFI, and hypervisor-backed integrity for selected kernel state, is largely aimed at slab-level corruption and control-flow hijacking. A stale mapping onto a freed physical page sits underneath most of that: it (typically) needs no leak, hijacks no control flow, and gives read/write access to whatever the page allocator hands out next. In practice that pushes the defense back to not having the bug in the first place, i.e., auditing drivers that map kernel-owned pages into userspace or device address spaces and making sure every such mapping is torn down before the page is released.

None of the kernel bugs in this series were subtle. Each is a page-lifetime mistake that a focused review of the driver would likely have caught, and none of the drivers gave the impression of having been written with an adversarial caller in mind. The intended line of defense is to keep such drivers out of reach: gate them behind a dedicated SELinux domain and let only a trusted component talk to them. That is a useful layer where it is actually applied. And even where it is, it is only as strong as the OEM's own userspace IPC surface into that domain, and the chains here show that logic bugs there are enough to cross it. It is not a substitute for treating the driver behind it as reachable.

The series continues with the per-OEM chains:

  • Part 2: Samsung
  • Part 3: Xiaomi
  • Part 4: Oppo, OnePlus, and Realme

If you find this work interesting, have questions, or notice technical inaccuracies, feel free to contact me at lukas.maar@calif.io.