
PoC and GDB script assists in triggering CVE-2025-38352
A reproducible Proof-of-Concept (PoC) and automated GDB orchestration script for CVE-2025-38352, a Time-of-Check to Time-of-Use (TOCTOU) race condition in the Linux kernel's POSIX CPU timer subsystem (kernel/time/posix-cpu-timers.c) leading to Use-After-Free (UAF).
Disclaimer: This project is for educational, defensive, and security research purposes only. All testing and reproduction were conducted in an isolated ARM64 QEMU virtual machine.
Process A creates child Process B.
Process B creates a CPU timer and begins exiting, entering the zombie state. Just as B is exiting, a timer interrupt arrives on its CPU core. The kernel enters interrupt context to process the timer, but temporarily releases its lock (unlock_task_sighand) to avoid deadlocks.
At the exact same time on another core, Process A reaps the exiting B, cleaning up its signal handler (sighand = NULL) and unhashing its PID.
Another thread in B calls timer delete (posix_cpu_timer_del). It attempts to look up the task via its PID and check its signal handler, but finds them already cleaned up / NULL. Misinterpreting that the task is completely dead and no timers could possibly be firing, it assumes it is safe and frees the timer from memory.
Meanwhile, the timer interrupt handler on the first core resumes to fire the expired timer. Because the timer was just freed in Step 4, accessing it triggers a Use-After-Free and crashes the kernel.
poc_gdb_script.py)Because QEMU's GDB stub does not support set non-stop on (halting one vCPU halts all vCPUs), standard multi-threaded synchronization cannot be achieved via plain breakpoints.
This repository solves the problem via In-Memory Instruction Patching:
Stage 1 (CPU Barrier):
exit_notify (CPU 2)kernel_wait4 (CPU 0)__arm64_sys_timer_delete (CPU 1)handle_posix_cpu_timers (CPU 2)$pc with the ARM64 self-branch opcode 0x14000000 (b . infinite loop) and resumes execution.Stage 2 (Sequential Orchestration):
set scheduler-locking on) and steps each vCPU forward sequentially:
handle_posix_cpu_timers past unlock_task_sighand().Download or clone the Android Common Kernel source tree at commit 1bf1aa362e6b9573a310fcd14f35bc875b42ba83:
# Option A: Download tarball directly
curl -LO https://android.googlesource.com/kernel/common/+archive/1bf1aa362e6b9573a310fcd14f35bc875b42ba83.tar.gz
mkdir -p kernel-cve && tar -xzf 1bf1aa362e6b9573a310fcd14f35bc875b42ba83.tar.gz -C kernel-cve
cd kernel-cve
# Option B: Clone repository
git clone https://android.googlesource.com/kernel/common
cd common
git checkout 1bf1aa362e6b9573a310fcd14f35bc875b42ba83
run_posix_cpu_timers()Open kernel/time/posix-cpu-timers.c, locate function run_posix_cpu_timers() (around line 1435–1448), and comment out the patch lines:
void run_posix_cpu_timers(void)
{
struct task_struct *tsk = current;
lockdep_assert_irqs_disabled();
/*
* Ensure that release_task(tsk) can't happen while
* handle_posix_cpu_timers() is running. Otherwise, a concurrent
* posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
* miss timer->it.cpu.firing != 0.
*/
// if (tsk->exit_state)
// return;
CONFIG_POSIX_CPU_TIMERS_TASK_WORKNote: When
CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, CPU timers are executed fromtask_workcontext instead of timer IRQ, bypassing this race condition. Therefore, it must be disabled to reproduce the vulnerability.
Because CONFIG_POSIX_CPU_TIMERS_TASK_WORK lacks a prompt string in upstream Kconfig, it defaults to y and cannot be toggled directly in menuconfig. You can expose it as follows:
kernel/time/Kconfig (around line 56), add a prompt string and change the default:
config POSIX_CPU_TIMERS_TASK_WORK
bool "POSIX CPU timers task work"
default n
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
scripts/config --disable POSIX_CPU_TIMERS_TASK_WORK
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make olddefconfig
.config:
grep POSIX_CPU_TIMERS_TASK_WORK .config
# Expected output: # CONFIG_POSIX_CPU_TIMERS_TASK_WORK is not set
Compile the ARM64 kernel image:
time ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make -j$(nproc) Image
Cross-compile poc.c:
aarch64-linux-gnu-gcc -ggdb3 ./poc.c -o poc
Launch your QEMU virtual machine with 4 vCPUs (-smp 4) and enable the GDB stub (-s flag, port 1234):
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-smp 4 \
-m 2G \
-kernel arch/arm64/boot/Image \
-append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1" \
... \
-s
From your host terminal, attach GDB to QEMU and load the orchestration script:
gdb-multiarch vmlinux
pwndbg> target remote :1234
pwndbg> source poc_gdb_script.py
pwndbg> continue
Inside the QEMU guest shell, run the compiled binary:
./poc
[!TIP] Timing & Troubleshooting:
If the timer fires prematurely before the target thread has reached the zombie state, GDB will log:[!] ERROR: handle_posix_cpu_timers fired before exit_state == EXIT_ZOMBIE
- Option 1: Simply re-run
./pocand source script GDB a few times.- Option 2: Increase
TIMER_FIRE_MSinpoc.c(e.g., from#define TIMER_FIRE_MS 10to20), recompilepoc.c, and run again. This grants the thread extra time to reachexit_notify()and transition intoEXIT_ZOMBIEbefore the timer expires.
https://github.com/user-attachments/assets/11481b5c-25f8-46c3-be3a-7b79a06a4948
kernel_wait4tsk->sighand = NULLtimer_delete to call release_posix_timer() (freeing sigq).