



Executive Summary
Part 1 ended with code execution inside Android’s mediacodec SELinux context — the “sandbox” AOSP documents as the place non-secure software decoders live. Seth Jenkins’s question in Part 2 is the one every 0-click chain actually cares about: what kernel drivers can that context open? Using DriverCartographer he found /dev/bigwave, the Pixel SoC’s AV1 decode accelerator. Of course the media sandbox can talk to the AV1 block. That is the point of the hardware. It is also, as years of Project Zero driver work have shown, the point of the LPE.
A couple of hours in the BigWave driver produced three bugs. The first was a duplicate of a February 2024 report still unfixed in June 2025 — a two-line transposition. The second was a linked-list analogue of a kmalloc double-free. The third, CVE-2025-36934, is the one that became the chain: a use-after-free of the per-fd inst/job against bigo_worker_thread after a 16-second ioctl timeout, turned into a 2144-byte write-what-where with no KASLR leak, then a reliable ashmem/configfs arbitrary read/write, SELinux permissive, and init_cred. All three were fixed 5 January 2026. The public write-up is 14 January. This draft keeps every original listing and both figures, then adds the kitchen picture, a race timeline, and the hunts a defender can run without the blob.
Android drivers for hardware devices are prime places to find powerful local privilege escalation bugs. The BigWave driver was no exception.
Seth Jenkins, Project Zero, 14 January 2026
mediacodec on Pixel 9 via /dev/bigwave. Not the Dolby bug (CVE-2025-54957, Part 1). Not Pixel 10 — Jenkins’s May 2026 follow-up says BigWave is gone there and the LPE moves to a VPU node. Fix: 5 January 2026. Unpatched December 2025 SPL is the lab.Read this in two voices
Green boxes are for a smart reader who does not live in container_of. Blue boxes are for people who will grep a Pixel kernel and write a syzkaller repro. The original post is a tight 2,600-word exploit diary. We keep its two figures and three listings in source order and add the maps Part 1 readers already expect.
- If you patch phones: this is why “the decoder sandbox” is not a security boundary once it can open an SoC video block.
- If you write drivers: an object whose lifetime is an fd cannot be owned by a worker thread that is not synced to that fd. Sixteen seconds is not a lock.
- If you hunt: mediacodec opening
/dev/bigwaveis normal. mediacodec spraying unix-socket kmalloc then closing BigWave fds after 16-second ioctls is not.
Why the sandbox has a hole shaped like AV1
mediacodec is supposed to be constrained. The AOSP media framework hardening page says so: non-secure software decoders run there. Hardware accelerators that those decoders need are also allowed to run there, because otherwise 4K AV1 would be a software problem. Jenkins’s DriverCartographer walk of the context turned up /dev/bigwave. BigWave is on-SoC silicon that accelerates AV1. Accessibility is a feature. The driver bugs are the incident.
Project Zero has been saying this out loud for years: Qualcomm DSP, Android drivers in general, in-the-wild Android, and the issue-tracker cousins 380081941, 42451599, 389724938. A sandboxed userspace that can open a GPU/VPU/DSP node is one ioctl away from the kernel’s allocator. Part 2 is that sentence with a Pixel part number.
ls -l /dev/bigwave, SELinux allow mediacodec on that chr_file, and whether the node exists on Pixel 10 (Jenkins: no — VPU instead). DriverCartographer-class tools: enumerate char devices reachable from a context, then read the ioctl table. That is a two-hour bug hunt only if the driver is this sloppy; budget more.The (Very Short) Bug Hunt
Jenkins is not being modest. He found three bugs in a couple of hours of reading. That is a driver-quality sentence, not a brag.
| # | Tracker | Shape | Why it mattered |
|---|---|---|---|
| 1 | 425917200 | Duplicate of a Feb 2024 report; still open in June 2025 | Fix was transposing two lines. A year of unfixed two-line bugs is a process failure, not a hard bug. |
| 2 | 426548270 | Double-free analogue on a different linked list | Fascinating class; not the chain. Read the issue if you collect list UAFs. |
| 3 | 426567975 / CVE-2025-36934 | Timeout UAF of inst/job vs bigo_worker_thread | Nicest primitive: 2144-byte write, controllable src and dst, no KASLR leak required. |
The year-long duplicate is the policy hook Part 3 will lean on. A two-line transposition sitting open from February 2024 to a January 2026 bulletins is not “Android is hard.” It is a driver that did not get a test that would have caught the swap, and a duplicate that did not get treated as a ticking CVE. Jenkins still picked bug three for the chain because nicest primitive beats oldest bug when you are writing a 0-click.
The Nicest Bug
Every open(/dev/bigwave) allocates a kernel inst and hangs it off file->private_data. Inside inst is an inline job: registers and status for one hardware invocation. Work is submitted with ioctl BIGO_IOCX_PROCESS. The ioctl copies BigWave register values from AP userland, places the job on a priority queue, and a separate thread — bigo_worker_thread — picks it up. An object whose lifetime is an fd is therefore accessed on a thread that is not explicitly synced to that fd.
After enqueue, the ioctl sits in wait_for_completion_timeout for 16 seconds. If the worker has not signaled, the ioctl dequeues the job and returns to userland. If you stacked enough previous jobs, the worker can be so far behind that it has only just dequeued the same job the ioctl now believes has timed out. Userland closes the fd. inst (and the inline job) is destroyed. The worker is still in bigo_run_job.


The highlights in the original listing are the UAF’d accesses. Verbatim:
static int bigo_worker_thread(void *data)
{
...
while(1) {
rc = wait_event_timeout(core->worker,
dequeue_prioq(core, &job, &should_stop),
msecs_to_jiffies(BIGO_IDLE_TIMEOUT_MS)); //The job is fetched from the queue
...
inst = container_of(job, struct bigo_inst, job); //The job is an inline struct inside of the inst which gets UAF'd
...
rc = bigo_run_job(core, job);
...
job->status = rc;
complete(&inst->job_comp);
}
return 0;
}
...
static int bigo_run_job(struct bigo_core *core, struct bigo_job *job)
{
...
inst = container_of(job, struct bigo_inst, job);
bigo_bypass_ssmt_pid(core, inst->is_decoder_usage);
bigo_push_regs(core, job->regs); //The register values of the bigwave processor are set (defined by userland)
bigo_core_enable(core);
ret = wait_for_completion_timeout(&core->frame_done,
msecs_to_jiffies(core->debugfs.timeout)); //pause for 1 second
...
//At this point inst/job have been freed
bigo_pull_regs(core, job->regs); //A pointer is taken directly from the freed object
*(u32 *)(job->regs + BIGO_REG_STAT) = status;
if (rc || ret)
rc = -ETIMEDOUT;
return rc;
}
And the write itself:
void bigo_pull_regs(struct bigo_core *core, void *regs)
{
memcpy_fromio(regs, core->base, core->regs_size); //And the current register values of the bigwave processor are written to that location
}
bigo_push_regs at the start of the job copies attacker-chosen register state onto the hardware. bigo_pull_regs at the end memcpy_fromios the current register file to job->regs. If you set the registers so the BigWave processor does not actually run, the end state is almost the start state. You control what is written. Spray attacker kmalloc (Jenkins’s example: Unix domain socket messages) over the freed inst, point job->regs where you like, and you have a 2144-byte write-what-where. No KASLR leak required for the write itself.

bigo_worker_thread lags past 16s, close the fd on timeout, spray UDS sendmsg into the inst slab, leave job->regs as a pointer you chose. Size 2144 is core->regs_size — a hardware register window, not a heap-spray aesthetic. Do not treat 16s as “slow.” It is an eternity for a worker and a free lunch for userland.Defeating KASLR (by doing nothing at all)
The textbook next step is: realloc something with a pointer where job->regs sits, corrupt the pointed-to object, maybe go cross-cache. Jenkins’s review of that plan: tedious, not fun. Instead he reused a Pixel-wide observation from “Defeating KASLR by Doing Nothing at All” (November 2025): you do not need the slide to smash kernel .data. You can use 0xffffff8000010000 — the linear map of the kernel — and write globals as if KASLR were off. That is not a leak. That is a constant. Reliability goes up because the first write no longer depends on a pointer you have not earned yet.

Limits, which Jenkins is honest about: this constant helps for kernel .data (globals, init_task, ashmem_misc, sel_fs_type). It does not give you the heap address of this particular inst. Anything that needs “where is my sprayed object” still needs a leak or a forge inside .data you already clobbered. The rest of the post is that distinction.
.data a stable alias is a KASLR bypass for every write-what-where that can target it. Read Jenkins’s November post before you argue this CVE “requires KASLR defeat.” The defeat shipped as a blog post two months earlier.Creating an arbitrary read/write
After the first write you can alias kernel globals. You cannot yet do a clean, small read/write. Two problems: complete() at the end of the worker loop, and the 2144-byte blast radius.
Forging the swait list so complete() does not panic
complete calls swake_up_locked on a list_head inside the UAF’d inst (&inst->job_comp.wait). Verbatim:
static inline int list_empty(const struct list_head *head)
{
return READ_ONCE(head->next) == head;
}
void swake_up_locked(struct swait_queue_head *q) //The q is located at &inst->job_comp.wait (so attacker controlled)
{
struct swait_queue *curr;
if (list_empty(&q->task_list))
return;
curr = list_first_entry(&q->task_list, typeof(*curr), task_list);
wake_up_process(curr->task);
list_del_init(&curr->task_list);
}
The easy forge — list_empty returning true — needs to know the inst’s heap address, because q is inline. Linear-map KASLR bypass does not give heap. So you forge a real-looking list: a q that points at a list entry, a task for wake_up_process, and enough nodes to survive list_del_init. The task is easy: init_task lives in kernel .data, reachable through the linear map. A spurious wake of init is noise. The list nodes are easy too, once the 2144-byte write has already given you a controlled region in .data: plant the nodes there, and plant pointers to those future nodes in the original heap spray that replaced inst. Jenkins points at setup_linked_list in the public exploit for the exact bytes. We are not reprinting that function.
ashmem_misc, configfs, and a 2144-byte paint roller
The goal is to turn a messy 2144-byte write into a quiet arbitrary read/write. Jenkins reimplements a trick he reversed from an in-the-wild Android exploit in 2023: type-confuse VFS handlers on ashmem_misc. CFI means you cannot point fops at arbitrary kernel text. You must swap VFS handlers for other VFS handlers. configfs handlers work, as they did in the ITW sample.

Green handlers treat private_data as struct ashmem_area (asma). Yellow handlers treat the same memory as a configfs buffer and touch page — that is the read/write window. The target address is set with the ASHMEM_SET_NAME ioctl. One catch: the linear map of kernel .text is not executable, so you cannot use linear-map addresses of the VFS handlers when forging the table. You need the real KASLR’d .text pointers.
So before ashmem_misc, the first .data write hits sel_fs_type. That object has a name string printed in /proc/self/mounts. Replace the string pointer, read mounts, and the unreliable write is now an arbitrary read. From there, read ashmem_fops through the linear map, subtract the known offset, and you have the slide. A second 2144-byte write overwrites ashmem_misc with a pointer to a forged fops table you planted in the same blast — the perk of writing far more than you need.
The perk is also the cost. 2144 bytes around the target die. Jenkins’s field note: the phone is “surprisingly quite stable,” except it seemed to crash when toggling Wi‑Fi. Otherwise it mostly works. That is not a reliability story you want in production. It is enough for a demo.
Once the forged ashmem_misc is in, arbitrary read/write is reliable (plus the occasional panic). He sets SELinux permissive by flipping selinux_state, forks, points the child task creds at init_cred. Root, SELinux down. Part 2’s security argument is over. The rest is packaging.
/proc/self/mounts right after a BigWave timeout-and-close is not a media player. Wi‑Fi toggle panics after a mediacodec incident are a forensic clue, not a root cause. CFI being the reason they had to use configfs fops is a win for CFI and a reminder that “other VFS handlers” are still a class.Integrating into the Dolby exploit
Two exploits are not a chain until they fit in the same process. Part 1’s Dolby payload plants bytes with /proc/self/mem and jumps. Part 2 therefore has to become a position-independent blob, much smaller than a statically linked 500 KB binary. First cut: drop libc, write syscall wrappers by hand. Jenkins looked at that chore, pasted the source into Gemini, and asked for a header. The header did not compile. He pasted the errors back. New errors. Four or five rounds later the header compiled and worked. 7 KB instead of 500 KB.
That paragraph is not a product endorsement. It is an operational note P0 chose to print: attackers will use (and likely already use) LLMs for the boring glue — syscall stubs, PIC glue, build errors — while the bug and the primitive stay human. An ELF is still not enough. The Dolby stage jumps to the start of the shellcode, not to a linker. Prepend a jump to the ELF entry, compile -mcmodel=tiny -fPIC -pie, and the blob does not care where Part 1 parked it or how it is aligned.
After 4 or 5 attempts, Gemini was able to generate a header file that not only compiled — it worked perfectly. This provides some insight into how attackers might be able to use (or more likely are already using) LLMs to make their exploit process more efficient.
Seth Jenkins, Integrating into the Dolby exploit
e_entry, planted via /proc/self/mem over a Dolby-chosen function, is the integration signature. If you are writing detections for “LLM-assisted exploits,” do not. Detect the ioctl race and the ashmem fops swap. The header file is not an IOC.Finalizing the exploit
Kernel read/write is the researcher demo. For a broader audience Jenkins added a stage that runs an included shell script: take a picture, send it to an IP. That is a demo of impact — the 0-click chain can become a camera — not a C2 framework. We are not reproducing the script. The series then hands off to Part 3 for the ecosystem argument: vendor blobs, 0-click media, driver sandboxes that are not sandboxes.
If you read Part 1: the voicemail was never opened, mediacodec ran Dolby, /proc/self/mem planted this blob, BigWave donated kernel R/W, and the demo used the camera. Two bugs, two posts, one message. That is the chain Google asked P0 to prove was possible on a modern Pixel.
A glossary for people who skipped Part 1
| Term | Kitchen | Operator |
|---|---|---|
| mediacodec | The locked mailroom that photocopies voicemail. | SELinux context for non-secure software decoders. |
| /dev/bigwave | The AV1 conveyor the mailroom is allowed to use. | Pixel SoC AV1 accelerator char device. |
| inst / job | Desk + ticket for one cook order. | Per-fd kernel struct; job is inline; lifetime should be the fd. |
| bigo_worker_thread | The cook, on a different clock from the waiter. | Kernel thread that dequeues jobs and talks to hardware. |
| 16s timeout | Waiter timer. Not a lock. | wait_for_completion_timeout then dequeue and return. |
| bigo_pull_regs | Copy oven dials onto the clipboard. | memcpy_fromio of 2144 bytes to job->regs. |
| linear map | Boiler room is always N steps from the east fence. | 0xffffff8000010000 as stable kernel .data alias on Pixel. |
| ashmem_misc | Front-desk phone script. | miscdevice fops; ITW type-confusion target. |
| configfs fops | A different official script CFI will accept. | VFS handlers swapped in because CFI blocks arbitrary .text. |
| init_cred | The night manager’s badge. | Kernel cred of init; pointed at by the forked task. |
The three bugs as a process smell, not a trophy case
A two-line transposition reported in February 2024 and still open in June 2025 is the sentence that should survive a CISO skim. The linked-list double-free analogue is the sentence that should survive a kernel-heap collector’s skim. CVE-2025-36934 is the sentence that should survive an incident responder’s skim. Shipping all three in one driver, reachable from the process that transcribes RCS audio, is the sentence that should survive an Android platform review.
Jenkins’s “couple of hours” is not a claim that every OEM driver falls this fast. It is a claim that this one did, using methods P0 has been publishing since the DSP and “driving forward” posts: map the context, open the nodes, read the ioctl paths, look for lifetime bugs across threads. If your SDL for SoC drivers does not include “fd-bound object accessed on a worker after a timeout,” you are waiting for the next Part 2.
Reliability, blast radius, and what the demo hides
Part 1’s userland was 1/256 because of two ASLR nibbles. Part 2’s kernel write is “mostly arbitrary” and 2144 bytes wide. The combination is not a stealth implant. Jenkins reports Wi‑Fi toggle panics after the paint roller hits .data. The forged ashmem path is the stabilization step; until then you are one wrong neighbor away from a reboot that looks like a radio bug.
The camera script is a press demo. A real operator would not need it: kernel R/W and init_cred are the impact. The demo exists because P0 wanted a picture — literally — of what 0-click means when the chain is done. Treat it as rhetoric, not as the payload you will see in the wild.
ATT&CK-shaped map of Part 2
| Stage | What P0 did | Defender residue |
|---|---|---|
| Entry | Part 1 shellcode in mediacodec | /proc/self/mem writes, Dolby crash-loop (Part 1) |
| Discovery | DriverCartographer: /dev/bigwave reachable | Normal for AV1; inventory anyway |
| Exploit | T1068-class LPE: timeout UAF + UDS spray | 16s BIGO_IOCX_PROCESS, close, sendmsg spray |
| KASLR | Linear map constant, then mounts leak | sel_fs_type name swap; odd /proc/self/mounts |
| Stabilize | ashmem/configfs fops confuse | Forged ashmem_misc; CFI-legal handler swap |
| Impact | selinux_state permissive, init_cred | Root task; camera demo optional |
A defensive lab without the blob
We do not host Jenkins’s exploit. On a device you own you can still answer the questions the post raises.
- Does
mediacodechaveallowon/dev/bigwave? If yes, this context is a kernel-driver context, not just a decoder context. - Does the ioctl path take a 16-second completion timeout and then free the fd-bound object without waiting for the worker? That is the bug class even after this CVE number is retired.
- Is the January 2026 SPL present? All three BigWave bugs rode that bulletin.
- On Pixel 10, is BigWave gone and a VPU node present instead? Read Jenkins’s May 2026 post before you close the ticket as “Pixel 9 only.”
- Can you reproduce a mounts-string mismatch after a media incident? That is the
sel_fs_typeread gadget, not a mount namespace bug.
# lab device / extracted policy — not an attack
# 1. node + context
ls -lZ /dev/bigwave 2>/dev/null
# 2. policy
sesearch -A -s mediacodec -t '*bigwave*' -c chr_file 2>/dev/null
# 3. running seccomp still matters from Part 1
pid=$(pidof mediacodec | awk '{print $1}')
grep -i seccomp /proc/$pid/status 2>/dev/null
# 4. image identity
getprop ro.build.version.security_patch
How Part 1 and Part 2 actually snap together
Part 1’s constraints (no execmem, no dlopen, /proc/self/mem, 1/256 userland) are why Part 2 had to shrink from 500 KB to 7 KB PIC. Part 2’s gift (a driver in the same context) is why Part 1’s “is mediacodec useful to an attacker?” question is answered in the affirmative. Neither post is complete alone. A Dolby-only crash is a decoder bug. A BigWave-only LPE is a local privilege escalation that needs a foothold. Together they are a voicemail.
The Gemini header is the punchline that will get quoted out of context. Leave it in context: the race, the linear map, the ashmem confusion, and the two-line bug that sat for a year are the story. The LLM wrote wrappers.
Key Takeaways
- mediacodec can open /dev/bigwave because AV1 acceleration is a feature. That makes the “decoder sandbox” a driver sandbox.
- CVE-2025-36934 is a 16-second timeout UAF: ioctl returns, close(fd), worker still in bigo_run_job, 2144-byte write via bigo_pull_regs.
- Two other bugs in the same driver: a year-old two-line duplicate, and a linked-list double-free analogue.
- KASLR for .data was already a constant (0xffffff8000010000) from Jenkins’s November 2025 post. The mounts/sel_fs_type trick earns the .text slide.
- Stabilization is an ITW-class ashmem/configfs fops swap under CFI, then selinux_state and init_cred.
- Integration with Dolby required a 7 KB PIC blob and a jump at the front; syscall stubs came from a Gemini loop. The blob is not in this draft.
- Fixed 5 January 2026. Pixel 10 drops BigWave and moves the LPE question to a VPU node.
Defensive Recommendations
- Patch. January 5, 2026 SPL. Confirm all three BigWave issues, not only CVE-2025-36934.
- Lifetime. Fd-bound objects must not be reachable from worker threads after close. Timeouts are not reference counts. Add a test that closes during a stacked-job delay.
- Sandbox. Treat every char device mediacodec can open as in-scope for LPE review. DriverCartographer should be a CI job, not a P0 surprise.
- KASLR. Read the November 2025 linear-map post. A write-what-where into .data is a KASLR bypass on current Pixel maps.
- CFI is not done. Swapping VFS handlers for other VFS handlers is the residual class. ashmem_misc is now a known target; audit siblings.
- Detect. 16s BigWave ioctls + close + UDS spray from mediacodec; mounts reads; Wi‑Fi-toggle panics after media incidents.
- Pixel 10. Do not close as “fixed, hardware gone.” Follow the VPU node with the same lifetime questions.
- Read Part 3. The ecosystem argument — blobs, transcription, driver sandboxes — is the reason these two posts exist.
Conclusion
Part 1 proved a voicemail can run code in the photocopier. Part 2 proves the photocopier had a key to the loading dock, and the dock’s forklift would keep using a clipboard after the desk was gone. Sixteen seconds, 2144 bytes, a constant instead of a leak, and a fops table CFI had to accept. The camera demo is for the people who still think 0-click is a slide. The cred swap is for everyone else. Patch the driver. Then go read what else mediacodec is allowed to open.
Original text: “A 0-click exploit chain for the Pixel 9 Part 2: Cracking the Sandbox with a Big Wave” by Seth Jenkins at Google Project Zero.


