From graphics driver to root shell: a tour of Samsung's kernel protections

Jamf Threat Labs analyzes Samsung kernel memory protections and bypass methods observed in the wild.

July 30 2026 by

Jamf Threat Labs

Authored by: Vladimir Pazukhin

Imagine you are an exploit developer, and you just found a driver vulnerability that gives you arbitrary physical memory read and write. On a stock Linux kernel, you would overwrite your process UID to zero and call it a day. But on Samsung Galaxy phone, you quickly discover that physical memory access is far from the end of the problem — all the simple ways of code execution lead to kernel panic and reboot.

Samsung provides four kernel hardening layers that exist precisely to make raw memory access harder for privilege escalation. To move forward, you’ll need to bypass these protections.

In this article we will cover this component of the exploit chain deeper than usual, discussing what each layer of kernel protection actually does, what techniques it blocks and how attackers get around it.

Table of contents:

  1. ARM exception levels: quick context
  2. Starting point: physical read/write via GPU
  3. Wall #1: RKP (Real-time Kernel Protection)
  4. Wall #2: KDP (Kernel Data Protection)
  5. Wall #3: DEFEX (Defeat Exploit)
  6. Wall #4: JOPP / ROPP (Control-flow integrity)
  7. The bypass: GPU DMA does not hit the CPU's Stage 2
  8. Exploitation Step 1 — the self-referencing page table trick
  9. Exploitation Step 2 — disabling SELinux
  10. Exploitation Step 3 — code injection
  11. So what?
  12. Conclusion

ARM exception levels: quick context

ARM processors separate privilege into Exception Levels (EL). EL0 is user space. EL1 is the kernel. EL2 is the hypervisor, which supervises the kernel itself. Samsung's protections run at EL2; even a fully compromised kernel cannot disable them. When the kernel tries to do something forbidden (e.g., modify a protected page table entry, write to a credential page), the CPU hardware generates a trap to the hypervisor, which inspects and blocks the operation.

One more piece of background: ARM supports two layers of address translation. The kernel manages Stage 1 translation, mapping virtual address to "intermediate physical addresses" for each process. The hypervisor manages Stage 2 translation, mapping those intermediate addresses to real physical addresses. The hypervisor can mark any physical page as read-only in Stage 2, and this restriction applies to all CPU accesses regardless of what Stage 1 says. This is the enforcement mechanism behind Samsung's memory protection layers, described below.

The important assumption: all memory modifications pass through the CPU.

Starting point: physical read/write via GPU

Qualcomm's KGSL (Kernel Graphics Support Layer) driver exposes /dev/kgsl-3d0 to applications. This is how apps submit rendering work to the Adreno GPU. In this research we use a vulnerability in KGSL command submission (CVE-2025-21479) that allows an attacker to tell the GPU to "reload your page tables from this physical address" (via a GPU-internal command called CP_SMMU_TABLE_UPDATE), then issue DMA (Direct Memory Access) reads and writes that resolve through attacker-controlled translations to arbitrary physical addresses. DMA means the GPU reads and writes system memory directly over the hardware bus without involving the CPU at all.

First, the exploit allocates a large number of memory buffers (256 regions of 16 MB each) and fills them with pre-crafted fake GPU page tables pointing at a target physical address. Then it discovers which physical address actually backs one of those buffers — this is done by writing a marker via GPU to a guessed address, then scanning all the allocated pages for that marker. Once one address is confirmed, we can build arbitrary read/write primitives on top.

The primitives are slow; each GPU round-trip takes about a second due to command-completion synchronization. But they work against any physical address, including kernel memory.

Wall #1: RKP (Real-time Kernel Protection)

RKP is Samsung's hypervisor-based page table guard. On ARM, the CPU translates virtual addresses to physical addresses through page tables stored in memory. Whoever controls page tables controls what memory any process can access. RKP makes sure that only the hypervisor can make certain page table changes.

Specifically, page table pages are marked read-only in the hypervisor's Stage 2 translation. If the kernel (or anything running on the CPU) tries to write to a page table page, the hardware generates a Stage 2 fault, and the hypervisor catches it and blocks the write. RKP also prevents creating new executable mappings, changing the kernel's page table base register or mapping page table pages as writable.

The obvious exploitation path — "we have physical write, we will modify my page table entries (PTEs) to map kernel memory into my address space" — does not work. If we write to a PTE from the CPU, RKP intercepts it. It does not matter that we are writing via physical address rather than virtual; the CPU's memory access still goes through Stage 2 translation where RKP is watching.

Wall #2: KDP (Kernel Data Protection)

KDP goes after the data we would want to modify once we have kernel access. Every process has a struct cred in the kernel containing its UID, GID and capability bitmask. Setting UID to 0 makes a process root. Setting capabilities to all ones grants every privileged operation.

KDP places credential structures on physically isolated pages that are marked read-only in the hypervisor's Stage 2 translation, the same mechanism as RKP. The kernel cannot modify credentials through normal stores — it must go through a hypervisor-mediated call that validates the new credentials. SELinux state structures get similar treatment.

So even with a physical write primitive, if we target a credential page from the CPU, the Stage 2 translation faults and the write is blocked.

Wall #3: DEFEX (Defeat Exploit)

Suppose we somehow get past KDP and manage to become UID 0. DEFEX is the next layer — a signed allowlist loaded at boot that dictates which executables can perform sensitive operations. Our process binary (”/data/app/com.kind_and_innocent_app/...”) is not on that list. DEFEX checks on execve, setuid transitions and module loading. Root UID without execution rights is not very useful.

Wall #4: JOPP / ROPP (Control-flow integrity)

The classic kernel exploit technique is to overwrite a function pointer in a kernel object to redirect execution to commit_creds(prepare_kernel_cred(0)). JOPP breaks this. Function pointers stored in memory are encrypted with a key that lives in an EL2-accessible system register. At each indirect call site, the pointer is decrypted before jumping. If we overwrite a pointer with a raw code address, it decrypts to garbage and the kernel panics.

ROPP does the same for return addresses; they are XORed with a per-thread secret when pushed to the stack.

We cannot redirect kernel control flow through data corruption without knowing the encryption key. And we do not have the key.

The bypass: GPU DMA does not hit the CPU's Stage 2

All four protections share one foundation: they intercept memory operations that go through the CPU. RKP and KDP rely on EL2 Stage 2 translation faults. JOPP/ROPP rely on CPU-executed indirect calls and returns. DEFEX relies on kernel hooks that execute on the CPU.

When the GPU's DMA engine writes to physical memory, that write travels from the GPU across the hardware bus directly to RAM. It does not pass through the CPU's translation tables or trigger Stage 2 faults. The hypervisor has no visibility into it, and the write simply lands in memory.

The GPU does have its own address translation unit (ARM calls it SMMU for System Memory Management Unit; it’s equivalent to Intel's IOMMU). But on these devices, it is configured permissively enough to reach kernel physical memory. This single architectural gap defeats almost the entire protection stack.

Exploitation Step 1 — the self-referencing page table trick

First, we need to graduate from the slow GPU read/write channel to fast CPU-speed access. We can use a well-known technique, “Dirty Pagetable”. Just allocate two adjacent virtual pages in our process, calling them map and page_map. Touch both so they get backed by physical memory. Now, using kernel reads through the (slow) GPU primitive, walk the kernel's page tables to locate the PTE (page table entry) that controls wherepage_map points in physical memory. Then, via GPU DMA, overwrite that PTE to point page_map at the page table page itself — the physical page that contains the PTEs for both map and page_map.

RKP does not see the write because it came from the GPU, not the CPU. After this one write, reading page_map[i] returns the i-th PTE in the table. Writing page_map[i] modifies it. We now have direct, CPU-speed control over your own process's page table entries:

Why does RKP miss this? The initial PTE corruption is done via GPU DMA. It writes directly to RAM without passing through the CPU's Stage 2 translation, so the hypervisor never intercepts it. After that, when the CPU writes to page_map[i], the access resolves through a Stage 1 entry that says "user, read-write" for what appears to be a normal user-space page. RKP's Stage 2 marks page table pages as read-only, but that protection is keyed on the kernel's own mappings of those pages; it does not anticipate that a user-space virtual address could be pointing at a page table page, because the only way that could happen is if someone corrupted a PTE outside the CPU. Which is exactly what we did.

From this point on, we have fast, arbitrary physical read/write without touching the GPU again.

Exploitation Step 2 — disabling SELinux

Remap the PTE window to point at the physical page containing selinux_state. Write zero to the enforcing byte. Done.

KDP was supposed to prevent this; selinux_state lives on a page that is read-only in the hypervisor's Stage 2. But we are writing through a page table entry that was corrupted by a GPU DMA write. The hypervisor's Stage 2 protection relies on controlling which Stage 1 mappings exist — and we created one it does not know about.

SELinux is now permissive.

Exploitation Step 3 — code injection

What's next? We still cannot just change our own credentials; KDP places struct cred on protected pages. The kernel allocates credentials through a dedicated hypervisor-mediated allocator, and RKP tracks which physical pages hold credential objects. Writing arbitrary values to a cred page (even via our PTE trick) triggers integrity checks that panic the kernel.

We also cannot patch kernel code directly. RKP marks all kernel text pages as read-only and non-writable at both Stage 1 and Stage 2 levels and blocks any attempt to create writable mappings to those physical pages. Our PTE trick controls our own process's page tables, but kernel code pages are additionally tracked — remapping them writable through any path triggers a hypervisor check.

We also cannot hijack function pointers because of JOPP. But we do not need to do any of this.

Instead, we write code directly into a running process's executable pages. The target is init (PID 1), the most privileged process on Android, running as root with full capabilities and on every DEFEX allowlist.

The procedure:

  1. Read the full kernel image from physical memory. Parse the kernel's symbol table (kallsyms — a compressed table of all function and variable addresses baked into the kernel image) to locate important addresses.
  2. Walk the kernel's task list via physical reads to find init's process descriptor (task_struct — the kernel's internal representation of a process). From it, extract the memory management structure that gives us init's page table root and code address range.
  3. Walk init's user-space page tables to find the physical pages backing its executable code. Look for a code cave, a run of zero bytes from alignment padding.
  4. Write ARM64 shellcode into the cave via GPU DMA. Init's code pages are mapped read-only and executable the kernel enforces this through page permissions, and even our PTE trick cannot create a writable mapping to file-backed executable pages without triggering consistency checks. But GPU DMA writes directly to the physical page backing the code, bypassing all page permission enforcement.
  5. Overwrite one of init's PLT (Procedure Linkage Table) entries to branch to the shellcode. The PLT is a table of jump targets that the dynamic linker fills in when a binary is loaded, and it is how shared library calls work on Android. These are plain address values in a data section, not encrypted function pointers. JOPP does not protect them.
  6. Trigger the hooked function by setting an Android system property (which causes init to call into the relevant PLT stub).
  7. Restore the original PLT entry and zero the cave.

So what?

The issue is not a single KGSL bug, but an architectural assumption: Samsung's entire protection stack assumes memory integrity is enforced by monitoring the CPU. The GPU is a DMA-capable peripheral — i.e., it can read and write physical memory independently of the CPU — and none of these layers monitor it.

Fixing this can be very challenging. GPU command streams need validation to prevent the page-table-reload command from pointing at arbitrary physical addresses or the hypervisor needs visibility into peripheral DMA, which is a much harder architectural change. Until then, any exploit of Qualcomm GPU can bypass all four protection layers of Samsung device. That’s why many offensive solutions which we see in the wild use device-specific GPU exploits to escalate privilege instead of using general Android vulnerabilities.

Conclusion

Samsung's kernel hardening is a serious protection that defeats some CPU-only exploitation paths. But the GPU sits outside the monitored perimeter, which provides a way to bypass protections. Until that exists, additional protection solutions, independent of the device's built-in kernel defenses, are necessary to protect users from advanced state-sponsored attacks.

References

  1. Qualcomm June 2025 Security Bulletin
  2. Exploiting CVE-2025-21479 on a Samsung S23
  3. Exploring Android ROOT via CVE-2025–21479
  4. Introduction to Samsung Knox mobile security
  5. Code used for this article is based on proof-of-concept by zhuowei

Read the latest research from Jamf Threat Labs.