core-jmp core-jmpdeath of core jump

A 0-click Exploit Chain for the Pixel 9, Part 2: Cracking the Sandbox with a Big Wave (CVE-2025-36934)

Seth Jenkins’s Part 2: from mediacodec to kernel R/W on Pixel 9 via CVE-2025-36934 in /dev/bigwave. A 16-second ioctl timeout UAF becomes a 2144-byte write, linear-map KASLR skip, ashmem/configfs confusion, SELinux permissive and init_cred. Fixed 5 January 2026. Two other BigWave bugs in the same hunt.

oxfemale September 15, 2026 24 min read 62 reads
Export PDF
A 0-click Exploit Chain for the Pixel 9, Part 2: Cracking the Sandbox with a Big Wave (CVE-2025-36934)
Original text: "A 0-click exploit chain for the Pixel 9 Part 2: Cracking the Sandbox with a Big Wave"Seth Jenkins, Google Project Zero (14 January 2026). Sequel to Part 1: Decoding Dolby. Bug: CVE-2025-36934. Part 3: lessons. Kernel listings and figures below are reproduced verbatim with attribution captions. This draft does not re-host the exploit blob or the camera-exfil demo script.
A locked mailroom with a hardware conveyor running through the wall
The sandbox is a locked mailroom. The AV1 accelerator is a conveyor the mailroom is allowed to use.
A kitchen ticket rail and a 16-second timer; the cook still reading a ticket already thrown away
Kitchen: the waiter starts a 16-second timer and leaves. The cook is still holding the ticket when the desk is hauled out.
Six-step diagram from Part 1 shellcode to init_cred on Pixel 9
Part 1 planted the blob. Part 2 walks it through /dev/bigwave to kernel read/write.

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
Kitchen table: Part 1 stole the photocopier in the basement (the audio decoder). Part 2 asks what keys hang on that photocopier’s wall. One of them opens a factory conveyor labeled AV1. The conveyor has a cook who keeps working from a ticket after the waiter has already thrown the desk in the dumpster. That is the bug.
For operators: Scope: kernel LPE from 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/bigwave is 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.

Kitchen table: A mailroom with no windows still has a chute to the loading dock, because packages have to leave. The chute is not a bug. The loading-dock forklift that keeps using a clipboard after the desk is gone is the bug. Auditing the chute is how you find the forklift.
For operators: Inventory: on a Pixel 9 image, 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.

#TrackerShapeWhy it mattered
1425917200Duplicate of a Feb 2024 report; still open in June 2025Fix was transposing two lines. A year of unfixed two-line bugs is a process failure, not a hard bug.
2426548270Double-free analogue on a different linked listFascinating class; not the chain. Read the issue if you collect list UAFs.
3426567975 / CVE-2025-36934Timeout UAF of inst/job vs bigo_worker_threadNicest primitive: 2144-byte write, controllable src and dst, no KASLR leak required.
Three bugs, one chain. All fixed 5 January 2026. Source: original article / issue tracker.

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.

For operators: CWE-416 (UAF) for CVE-2025-36934; CWE-362 (race) on the timeout vs worker; CWE-668 if you want the “sandbox can open the accelerator” row. Do not file the KASLR linear-map trick as part of this CVE — that is the November 2025 Pixel-wide observation, reused here.

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.

Timeline of the 16-second ioctl timeout versus the worker thread
Sixteen seconds is a kitchen timer, not a lock. The worker can still be holding the ticket.
A worker still holding a clipboard after the desk was hauled away
Kitchen: close(fd) is throwing the desk away. bigo_pull_regs is still writing on the clipboard.

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.

Project Zero diagram of the BigWave UAF write primitive
2144-byte arbitrary write from bigo_pull_regs after the inst is freed. Source: original article.
Kitchen table: You hand the cook a ticket that says “set the oven dials to these numbers, wait one second, then copy the dials onto this clipboard.” You start a 16-second waiter timer. If the kitchen is backed up, the waiter gives up, you throw the clipboard’s desk in the dumpster, and you have already taped a new clipboard of your own onto the same hook. The cook copies the dials onto your clipboard. The dials are still the numbers you set, because you told the oven not to cook. That is write-what-where, 2144 bytes, no need to ask anyone for the building’s address.
For operators: Race recipe (lab you own, patched or with the CVE): stack jobs so 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.

A master key ring on a dark metal data-floor, one key glowing
Kitchen: the building directory is always in the same drawer even if the street number is random. Linear map is that drawer.

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.

For operators: If you are hardening Pixel-class kernels: a linear map that makes .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.
Kitchen table: ASLR is changing the building’s street number every boot. The linear map is the rule that the boiler room is always “walk 0xffffff8000010000 steps from the east fence.” You never asked the concierge for the street number. You walked from the fence.

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.

Kitchen table: After the cook copies the dials, someone rings a bell that walks a guest list on the desk you already threw away. You cannot pretend the list is empty unless you know the desk’s old address. So you leave a fake guest list in the building directory (kernel .data) and pin the crumpled ticket to “wake the night manager” (init). The night manager shrugs. The bell does not take the kitchen down.

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.

Forged ashmem_misc fops table mixing ashmem and configfs handlers
Green: ashmem_area view of private_data. Yellow: configfs buffer view; page is the R/W window, aimed with ASHMEM_SET_NAME. Source: original article.

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.

For operators: Detection leftovers: a userspace that reads /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.
Kitchen table: You cannot replace the front-desk phone script with a random page from the employee handbook — the binder (CFI) checks the handwriting. You can swap in another official script (configfs) that happens to read a different binder. Then you tell the desk, via ASHMEM_SET_NAME, which drawer to open. First you retitle the building directory (sel_fs_type) so the lobby screen (/proc/self/mounts) reads out an address you chose. That is the leak. Then you swap the scripts. Then you photocopy init’s badge onto a new hire.

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
Kitchen table: Part 1 left a cassette in the photocopier. Part 2 has to be a song that plays if you drop the needle at the start of the tape, not a concert that needs a stage manager (the dynamic linker) and a 500 KB orchestra. The intern (an LLM) wrote the boring sheet music for the syscalls. The arrangement — tiny, PIC, a jump at the top — is still the adult in the room.
For operators: If you are matching this in a lab: a 7 KB PIC blob, no libc, syscall stubs, a leading branch to 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

TermKitchenOperator
mediacodecThe locked mailroom that photocopies voicemail.SELinux context for non-secure software decoders.
/dev/bigwaveThe AV1 conveyor the mailroom is allowed to use.Pixel SoC AV1 accelerator char device.
inst / jobDesk + ticket for one cook order.Per-fd kernel struct; job is inline; lifetime should be the fd.
bigo_worker_threadThe cook, on a different clock from the waiter.Kernel thread that dequeues jobs and talks to hardware.
16s timeoutWaiter timer. Not a lock.wait_for_completion_timeout then dequeue and return.
bigo_pull_regsCopy oven dials onto the clipboard.memcpy_fromio of 2144 bytes to job->regs.
linear mapBoiler room is always N steps from the east fence.0xffffff8000010000 as stable kernel .data alias on Pixel.
ashmem_miscFront-desk phone script.miscdevice fops; ITW type-confusion target.
configfs fopsA different official script CFI will accept.VFS handlers swapped in because CFI blocks arbitrary .text.
init_credThe night manager’s badge.Kernel cred of init; pointed at by the forked task.
Dual-audience glossary. Terms from the original article; kitchen column is ours.

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.

For operators: IR: a Pixel 9 that panics on Wi‑Fi toggle after a burst of mediacodec BigWave ioctls and a mounts read is closer to this post than to a modem bug. Do not wait for a photo on a C2. The cred swap is the compromise.

ATT&CK-shaped map of Part 2

StageWhat P0 didDefender residue
EntryPart 1 shellcode in mediacodec/proc/self/mem writes, Dolby crash-loop (Part 1)
DiscoveryDriverCartographer: /dev/bigwave reachableNormal for AV1; inventory anyway
ExploitT1068-class LPE: timeout UAF + UDS spray16s BIGO_IOCX_PROCESS, close, sendmsg spray
KASLRLinear map constant, then mounts leaksel_fs_type name swap; odd /proc/self/mounts
Stabilizeashmem/configfs fops confuseForged ashmem_misc; CFI-legal handler swap
Impactselinux_state permissive, init_credRoot task; camera demo optional
Mapped from techniques in the post, not a claim every cell is an official ATT&CK mobile ID.

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.

  1. Does mediacodec have allow on /dev/bigwave? If yes, this context is a kernel-driver context, not just a decoder context.
  2. 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.
  3. Is the January 2026 SPL present? All three BigWave bugs rode that bulletin.
  4. 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.”
  5. Can you reproduce a mounts-string mismatch after a media incident? That is the sel_fs_type read 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

  1. Patch. January 5, 2026 SPL. Confirm all three BigWave issues, not only CVE-2025-36934.
  2. 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.
  3. Sandbox. Treat every char device mediacodec can open as in-scope for LPE review. DriverCartographer should be a CI job, not a P0 surprise.
  4. KASLR. Read the November 2025 linear-map post. A write-what-where into .data is a KASLR bypass on current Pixel maps.
  5. CFI is not done. Swapping VFS handlers for other VFS handlers is the residual class. ashmem_misc is now a known target; audit siblings.
  6. Detect. 16s BigWave ioctls + close + UDS spray from mediacodec; mounts reads; Wi‑Fi-toggle panics after media incidents.
  7. Pixel 10. Do not close as “fixed, hardware gone.” Follow the VPU node with the same lifetime questions.
  8. 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.

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion