|
| 1 | +# Adreno A7xx SDS->RB privilege bypass (GPU SMMU takeover to Kernel R/W) |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +This page abstracts an in-the-wild Adreno A7xx microcode logic bug (CVE-2025-21479) into reproducible exploitation techniques: abusing IB-level masking in Set Draw State (SDS) to execute privileged GPU packets from an unprivileged app, pivoting to GPU SMMU takeover and then to a fast, stable kernel R/W via a dirty-pagetable trick. |
| 6 | + |
| 7 | +- Affected: Qualcomm Adreno A7xx GPU firmware prior to a microcode fix that changed masking of register $12 from 0x3 to 0x7. |
| 8 | +- Primitive: Execute privileged CP packets (e.g., CP_SMMU_TABLE_UPDATE) from SDS, which is user-controlled. |
| 9 | +- Outcome: Arbitrary physical/virtual kernel memory R/W, SELinux disable, root. |
| 10 | +- Prereq: Ability to create a KGSL GPU context and submit command buffers that enter SDS (normal app capability). |
| 11 | + |
| 12 | +## Background: IB levels, SDS and the $12 mask |
| 13 | + |
| 14 | +- The kernel maintains a ringbuffer (RB=IB0). Userspace submits IB1 via CP_INDIRECT_BUFFER, chaining to IB2/IB3. |
| 15 | +- SDS is a special command stream entered via CP_SET_DRAW_STATE: |
| 16 | + - A6xx: SDS is treated as IB3 |
| 17 | + - A7xx: SDS moved to IB4 |
| 18 | +- Microcode tracks the current IB level in register $12 and gates privileged packets so they are only accepted when the effective level corresponds to IB0 (kernel RB). |
| 19 | +- Bug: A7xx microcode kept masking $12 with 0x3 (2 bits) instead of 0x7 (3 bits). Since IB4 & 0x3 == 0, SDS was misidentified as IB0, allowing privileged packets from user-controlled SDS. |
| 20 | + |
| 21 | +Why it matters: |
| 22 | + |
| 23 | +``` |
| 24 | +A6XX | A7XX |
| 25 | +RB & 3 == 0 | RB & 3 == 0 |
| 26 | +IB1 & 3 == 1 | IB1 & 3 == 1 |
| 27 | +IB2 & 3 == 2 | IB2 & 3 == 2 |
| 28 | +IB3 (SDS) & 3 == 3 | IB3 & 3 == 3 |
| 29 | + | IB4 (SDS) & 3 == 0 <-- misread as IB0 if mask is 0x3 |
| 30 | +``` |
| 31 | + |
| 32 | +Microcode diff example (patch switched the mask to 0x7): |
| 33 | + |
| 34 | +``` |
| 35 | +@@ CP_SMMU_TABLE_UPDATE |
| 36 | +- and $02, $12, 0x3 |
| 37 | ++ and $02, $12, 0x7 |
| 38 | +@@ CP_FIXED_STRIDE_DRAW_TABLE |
| 39 | +- and $02, $12, 0x3 |
| 40 | ++ and $02, $12, 0x7 |
| 41 | +``` |
| 42 | + |
| 43 | +## Exploitation overview |
| 44 | + |
| 45 | +Goal: From SDS (misread as IB0) issue privileged CP packets to re-point the GPU SMMU to attacker-crafted page tables, then use GPU copy/write packets for arbitrary physical R/W. Finally, pivot to a fast CPU-side R/W via dirty pagetable. |
| 46 | + |
| 47 | +High-level chain |
| 48 | +- Craft a fake GPU pagetable in shared memory |
| 49 | +- Enter SDS and execute: |
| 50 | + - CP_SMMU_TABLE_UPDATE -> switch to fake pagetable |
| 51 | + - CP_MEM_WRITE / CP_MEM_TO_MEM -> implement write/read primitives |
| 52 | + - CP_SET_DRAW_STATE with run-now flags (dispatch immediately) |
| 53 | + |
| 54 | +GPU R/W primitives via fake pagetable |
| 55 | +- Write: CP_MEM_WRITE to an attacker-chosen GPU VA whose PTEs you map to a chosen PA -> arbitrary physical write |
| 56 | +- Read: CP_MEM_TO_MEM copies 4/8 bytes from target PA to a userspace-shared buffer (batch for larger reads) |
| 57 | + |
| 58 | +Notes |
| 59 | +- Each Android process gets a KGSL context (IOCTL_KGSL_GPU_CONTEXT_CREATE). Switching contexts normally updates SMMU tables in the RB; the bug lets you do it in SDS. |
| 60 | +- Excessive GPU traffic can cause UI blackouts and reboots; reads are small (4/8B) and sync is slow by default. |
| 61 | + |
| 62 | +## Building the SDS command sequence |
| 63 | + |
| 64 | +- Spray a fake GPU pagetable into shared memory so at least one instance lands at a known physical address (e.g., via allocator grooming and repetition). |
| 65 | +- Construct an SDS buffer containing, in order: |
| 66 | + 1) CP_SMMU_TABLE_UPDATE to the physical address of the fake pagetable |
| 67 | + 2) One or more CP_MEM_WRITE and/or CP_MEM_TO_MEM packets to implement R/W using your new translations |
| 68 | + 3) CP_SET_DRAW_STATE with flags to run-now |
| 69 | + |
| 70 | +The exact packet encodings vary by firmware; use freedreno’s afuc/packet docs to assemble the words, and ensure the SDS submission path is taken by the driver. |
| 71 | + |
| 72 | +## Finding Samsung kernel physbase under physical KASLR |
| 73 | + |
| 74 | +Samsung randomizes the kernel physical base within a known region on Snapdragon devices. Brute-force the expected range and look for the first 16 bytes of _stext. |
| 75 | + |
| 76 | +Representative loop |
| 77 | + |
| 78 | +```c |
| 79 | +while (!ctx->kernel.pbase) { |
| 80 | + offset += 0x8000; |
| 81 | + uint64_t d1 = kernel_physread_u64(ctx, base + offset); |
| 82 | + if (d1 != 0xd10203ffd503233f) continue; // first 8 bytes of _stext |
| 83 | + uint64_t d2 = kernel_physread_u64(ctx, base + offset + 8); |
| 84 | + if (d2 == 0x910083fda9027bfd) { // second 8 bytes of _stext |
| 85 | + ctx->kernel.pbase = base + offset - 0x10000; |
| 86 | + break; |
| 87 | + } |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +Once physbase is known, compute the kernel virtual with the linear map: |
| 92 | + |
| 93 | +``` |
| 94 | +_stext = 0xffffffc008000000 + (Kernel Code & ~0xa8000000) |
| 95 | +``` |
| 96 | + |
| 97 | +## Stabilizing to fast, reliable CPU-side kernel R/W (dirty pagetable) |
| 98 | + |
| 99 | +GPU R/W is slow and small-granularity. Pivot to a fast/stable primitive by corrupting your own process PTEs (“dirty pagetable”): |
| 100 | + |
| 101 | +Steps |
| 102 | +- Locate current task_struct -> mm_struct -> mm_struct->pgd using the slow GPU R/W primitives |
| 103 | +- mmap two adjacent userspace pages A and B (e.g., at 0x1000) |
| 104 | +- Walk PGD->PMD->PTE to resolve A/B’s PTE physical addresses (helpers: get_pgd_offset, get_pmd_offset, get_pte_offset) |
| 105 | +- Overwrite B’s PTE to point to the last-level pagetable managing A/B with RW attributes (phys_to_readwrite_pte) |
| 106 | +- Write via B’s VA to mutate A’s PTE to map target PFNs; read/write kernel memory via A’s VA, flushing TLB until a sentinel flips |
| 107 | + |
| 108 | +<details> |
| 109 | +<summary>Example dirty-pagetable pivot snippet</summary> |
| 110 | + |
| 111 | +```c |
| 112 | +uint64_t *map = mmap((void*)0x1000, PAGE_SIZE*2, PROT_READ|PROT_WRITE, |
| 113 | + MAP_PRIVATE|MAP_ANONYMOUS, 0, 0); |
| 114 | +uint64_t *page_map = (void*)((uint64_t)map + PAGE_SIZE); |
| 115 | +page_map[0] = 0x4242424242424242; |
| 116 | + |
| 117 | +uint64_t tsk = get_curr_task_struct(ctx); |
| 118 | +uint64_t mm = kernel_vread_u64(ctx, tsk + OFFSETOF_TASK_STRUCT_MM); |
| 119 | +uint64_t mm_pgd = kernel_vread_u64(ctx, mm + OFFSETOF_MM_PGD); |
| 120 | + |
| 121 | +uint64_t pgd_off = get_pgd_offset((uint64_t)map); |
| 122 | +uint64_t phys_pmd = kernel_vread_u64(ctx, mm_pgd + pgd_off) & ~((1<<12)-1); |
| 123 | +uint64_t pmd_off = get_pmd_offset((uint64_t)map); |
| 124 | +uint64_t phys_pte = kernel_pread_u64(ctx, phys_pmd + pmd_off) & ~((1<<12)-1); |
| 125 | +uint64_t pte_off = get_pte_offset((uint64_t)map); |
| 126 | +uint64_t pte_addr = phys_pte + pte_off; |
| 127 | +uint64_t new_pte = phys_to_readwrite_pte(pte_addr); |
| 128 | +kernel_write_u64(ctx, pte_addr + 8, new_pte, false); |
| 129 | +while (page_map[0] == 0x4242424242424242) flush_tlb(); |
| 130 | +``` |
| 131 | +
|
| 132 | +</details> |
| 133 | +
|
| 134 | +## Detection and hardening |
| 135 | +
|
| 136 | +- Firmware/microcode: fix all sites masking $12 to use 0x7 (A7xx) and audit privileged packet gates |
| 137 | +- Driver: validate effective IB level for privileged packets and enforce per-context allowlists |
| 138 | +- Telemetry: alert if CP_SMMU_TABLE_UPDATE (or similar privileged opcodes) appears outside RB/IB0, especially in SDS; monitor anomalous bursts of 4/8-byte CP_MEM_TO_MEM and excessive TLB flush patterns |
| 139 | +- Kernel: harden pagetable metadata and detect user PTE corruption patterns |
| 140 | +
|
| 141 | +## Impact |
| 142 | +
|
| 143 | +A local app with GPU access can execute privileged GPU packets, hijack the GPU SMMU, achieve arbitrary kernel physical/virtual R/W, disable SELinux and obtain root on affected Snapdragon A7xx devices (e.g., Samsung S23). Severity: High (kernel compromise). |
| 144 | +
|
| 145 | +## References |
| 146 | +
|
| 147 | +- [CVE-2025-21479: Adreno A7xx SDS->RB privilege bypass to kernel R/W (Samsung S23)](https://xploitbengineer.github.io/CVE-2025-21479) |
| 148 | +- [Mesa freedreno afuc disassembler README (microcode + packets)](https://gitlab.freedesktop.org/mesa/mesa/-/blob/c0f56fc64cad946d5c4fda509ef3056994c183d9/src/freedreno/afuc/README.rst) |
| 149 | +- [Google Project Zero: Attacking Qualcomm Adreno GPU (SMMU takeover via CP packets)](https://googleprojectzero.blogspot.com/2020/09/attacking-qualcomm-adreno-gpu.html) |
| 150 | +- [Dirty pagetable (archive)](https://web.archive.org/web/20240425043203/https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html) |
| 151 | +
|
| 152 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments