daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

posix-cpu-timers-toctou-cve-2025-38352.md (21494B)


      1 ---
      2 title: "POSIX CPU Timers TOCTOU race (CVE-2025-38352)"
      3 section: "Linux"
      4 sectionSlug: "linux-hardening"
      5 sourcePath: "src/linux-hardening/main-system-information/kernel-lpe-cves/posix-cpu-timers-toctou-cve-2025-38352.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/main-system-information/kernel-lpe-cves/posix-cpu-timers-toctou-cve-2025-38352.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # POSIX CPU Timers TOCTOU race (CVE-2025-38352)
     14 
     15 This page documents a TOCTOU race condition in Linux/Android POSIX CPU timers that can corrupt timer state and crash the kernel, and under some circumstances be steered toward privilege escalation.<sup>[[1]](#references)[[2]](#references)</sup>
     16 
     17 - Affected component: kernel/time/posix-cpu-timers.c.<sup>[[1]](#references)</sup>
     18 - Primitive: expiry vs deletion race under task exit.<sup>[[1]](#references)</sup>
     19 - Config sensitive: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path).<sup>[[1]](#references)</sup>
     20 
     21 Quick internals recap (relevant for exploitation):<sup>[[1]](#references)</sup>
     22 - Three CPU clocks drive accounting for timers via cpu_clock_sample():<sup>[[1]](#references)</sup>
     23   - CPUCLOCK_PROF: utime + stime
     24   - CPUCLOCK_VIRT: utime only
     25   - CPUCLOCK_SCHED: task_sched_runtime()
     26 - Timer creation wires a timer to a task/pid and initializes the timerqueue nodes:<sup>[[1]](#references)</sup>
     27 
     28 ```c
     29 static int posix_cpu_timer_create(struct k_itimer *new_timer) {
     30     struct pid *pid;
     31     rcu_read_lock();
     32     pid = pid_for_clock(new_timer->it_clock, false);
     33     if (!pid) { rcu_read_unlock(); return -EINVAL; }
     34     new_timer->kclock = &clock_posix_cpu;
     35     timerqueue_init(&new_timer->it.cpu.node);
     36     new_timer->it.cpu.pid = get_pid(pid);
     37     rcu_read_unlock();
     38     return 0;
     39 }
     40 ```
     41 
     42 - Arming inserts into a per-base timerqueue and may update the next-expiry cache:<sup>[[1]](#references)</sup>
     43 
     44 ```c
     45 static void arm_timer(struct k_itimer *timer, struct task_struct *p) {
     46     struct posix_cputimer_base *base = timer_base(timer, p);
     47     struct cpu_timer *ctmr = &timer->it.cpu;
     48     u64 newexp = cpu_timer_getexpires(ctmr);
     49     if (!cpu_timer_enqueue(&base->tqhead, ctmr)) return;
     50     if (newexp < base->nextevt) base->nextevt = newexp;
     51 }
     52 ```
     53 
     54 - Fast path avoids expensive processing unless cached expiries indicate possible firing:<sup>[[1]](#references)</sup>
     55 
     56 ```c
     57 static inline bool fastpath_timer_check(struct task_struct *tsk) {
     58     struct posix_cputimers *pct = &tsk->posix_cputimers;
     59     if (!expiry_cache_is_inactive(pct)) {
     60         u64 samples[CPUCLOCK_MAX];
     61         task_sample_cputime(tsk, samples);
     62         if (task_cputimers_expired(samples, pct))
     63             return true;
     64     }
     65     return false;
     66 }
     67 ```
     68 
     69 - Expiration collects expired timers, marks them firing, moves them off the queue; actual delivery is deferred:<sup>[[1]](#references)</sup>
     70 
     71 ```c
     72 #define MAX_COLLECTED 20
     73 static u64 collect_timerqueue(struct timerqueue_head *head,
     74                               struct list_head *firing, u64 now) {
     75     struct timerqueue_node *next; int i = 0;
     76     while ((next = timerqueue_getnext(head))) {
     77         struct cpu_timer *ctmr = container_of(next, struct cpu_timer, node);
     78         u64 expires = cpu_timer_getexpires(ctmr);
     79         if (++i == MAX_COLLECTED || now < expires) return expires;
     80         ctmr->firing = 1;                           // critical state
     81         rcu_assign_pointer(ctmr->handling, current);
     82         cpu_timer_dequeue(ctmr);
     83         list_add_tail(&ctmr->elist, firing);
     84     }
     85     return U64_MAX;
     86 }
     87 ```
     88 
     89 Two expiry-processing modes:<sup>[[1]](#references)</sup>
     90 - CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: expiry is deferred via task_work on the target task
     91 - CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: expiry handled directly in IRQ context
     92 
     93 <details>
     94 <summary>Task_work vs IRQ expiry paths</summary>
     95 
     96 ```c
     97 void run_posix_cpu_timers(void) {
     98     struct task_struct *tsk = current;
     99     __run_posix_cpu_timers(tsk);
    100 }
    101 #ifdef CONFIG_POSIX_CPU_TIMERS_TASK_WORK
    102 static inline void __run_posix_cpu_timers(struct task_struct *tsk) {
    103     if (WARN_ON_ONCE(tsk->posix_cputimers_work.scheduled)) return;
    104     tsk->posix_cputimers_work.scheduled = true;
    105     task_work_add(tsk, &tsk->posix_cputimers_work.work, TWA_RESUME);
    106 }
    107 #else
    108 static inline void __run_posix_cpu_timers(struct task_struct *tsk) {
    109     lockdep_posixtimer_enter();
    110     handle_posix_cpu_timers(tsk);                  // IRQ-context path
    111     lockdep_posixtimer_exit();
    112 }
    113 #endif
    114 ```
    115 
    116 </details>
    117 
    118 In the IRQ-context path, the firing list is processed outside sighand.<sup>[[1]](#references)</sup>
    119 
    120 <details>
    121 <summary>IRQ-context delivery loop</summary>
    122 
    123 ```c
    124 static void handle_posix_cpu_timers(struct task_struct *tsk) {
    125     struct k_itimer *timer, *next; unsigned long flags, start;
    126     LIST_HEAD(firing);
    127     if (!lock_task_sighand(tsk, &flags)) return;   // may fail on exit
    128     do {
    129         start = READ_ONCE(jiffies); barrier();
    130         check_thread_timers(tsk, &firing);
    131         check_process_timers(tsk, &firing);
    132     } while (!posix_cpu_timers_enable_work(tsk, start));
    133     unlock_task_sighand(tsk, &flags);              // race window opens here
    134     list_for_each_entry_safe(timer, next, &firing, it.cpu.elist) {
    135         int cpu_firing;
    136         spin_lock(&timer->it_lock);
    137         list_del_init(&timer->it.cpu.elist);
    138         cpu_firing = timer->it.cpu.firing;         // read then reset
    139         timer->it.cpu.firing = 0;
    140         if (likely(cpu_firing >= 0)) cpu_timer_fire(timer);
    141         rcu_assign_pointer(timer->it.cpu.handling, NULL);
    142         spin_unlock(&timer->it_lock);
    143     }
    144 }
    145 ```
    146 
    147 </details>
    148 
    149 Root cause: TOCTOU between IRQ-time expiry and concurrent deletion under task exit.<sup>[[1]](#references)</sup>
    150 Preconditions:<sup>[[1]](#references)</sup>
    151 - CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ path in use)
    152 - The target task is exiting but not fully reaped
    153 - Another thread concurrently calls posix_cpu_timer_del() for the same timer
    154 
    155 Sequence:<sup>[[1]](#references)</sup>
    156 1) update_process_times() triggers run_posix_cpu_timers() in IRQ context for the exiting task.
    157 2) collect_timerqueue() sets ctmr->firing = 1 and moves the timer to the temporary firing list.
    158 3) handle_posix_cpu_timers() drops sighand via unlock_task_sighand() to deliver timers outside the lock.
    159 4) Immediately after unlock, the exiting task can be reaped; a sibling thread executes posix_cpu_timer_del().
    160 5) In this window, posix_cpu_timer_del() may fail to acquire state via cpu_timer_task_rcu()/lock_task_sighand() and thus skip the normal in-flight guard that checks timer->it.cpu.firing. Deletion proceeds as if not firing, corrupting state while expiry is being handled, leading to crashes/UB.<sup>[[1]](#references)</sup>
    161 
    162 ### How release_task() and timer_delete() free firing timers
    163 Even after handle_posix_cpu_timers() has taken the timer off the task list, a ptraced zombie can still be reaped. The waitpid() stack drives release_task() → __exit_signal(), which tears down sighand and the signal queues while another CPU is still holding pointers to the timer object:<sup>[[4]](#references)</sup>
    164 
    165 ```c
    166 static void __exit_signal(struct task_struct *tsk)
    167 {
    168     struct sighand_struct *sighand = lock_task_sighand(tsk, NULL);
    169     // ... signal cleanup elided ...
    170     tsk->sighand = NULL;             // makes future lock_task_sighand() fail
    171     unlock_task_sighand(tsk, NULL);
    172 }
    173 ```
    174 
    175 With sighand detached, timer_delete() still returns success because posix_cpu_timer_del() leaves `ret = 0` when locking fails, so the syscall proceeds to free the object via RCU:<sup>[[4]](#references)</sup>
    176 
    177 ```c
    178 static int posix_cpu_timer_del(struct k_itimer *timer)
    179 {
    180     struct sighand_struct *sighand = lock_task_sighand(p, &flags);
    181     if (unlikely(!sighand))
    182         goto out;                   // ret stays 0 -> userland sees success
    183     // ... normal unlink path ...
    184 }
    185 ```
    186 
    187 ```c
    188 SYSCALL_DEFINE1(timer_delete, timer_t, timer_id)
    189 {
    190     if (timer_delete_hook(timer) == TIMER_RETRY)
    191         timer = timer_wait_running(timer, &flags);
    192     posix_timer_unhash_and_free(timer);            // call_rcu(k_itimer_rcu_free)
    193     return 0;
    194 }
    195 ```
    196 
    197 Because the slab object is RCU-freed while IRQ context still walks the `firing` list, reuse of the timer cache becomes a UAF primitive.<sup>[[4]](#references)</sup>
    198 
    199 ### Steering reaping with ptrace + waitpid
    200 The easiest way to keep a zombie around without it being auto-reaped is to ptrace a non-leader worker thread. exit_notify() first sets `exit_state = EXIT_ZOMBIE` and only transitions to EXIT_DEAD if `autoreap` is true. For ptraced threads, `autoreap = do_notify_parent()` remains false as long as SIGCHLD is not ignored, so release_task() only runs when the parent explicitly calls waitpid():<sup>[[4]](#references)</sup>
    201 
    202 - Use pthread_create() inside the tracee so the victim is not the thread-group leader (wait_task_zombie() handles ptraced non-leaders).
    203 - Parent issues `ptrace(PTRACE_ATTACH, tid)` and later `waitpid(tid, __WALL)` to drive do_wait_pid() → wait_task_zombie() → release_task().
    204 - Pipes or shared memory convey the exact TID to the parent so the correct worker is reaped on demand.
    205 
    206 This choreography guarantees a window where handle_posix_cpu_timers() can still reference `tsk->sighand`, while a subsequent waitpid() tears it down and allows timer_delete() to reclaim the same k_itimer object.<sup>[[4]](#references)</sup>
    207 
    208 Why TASK_WORK mode is safe by design:<sup>[[1]](#references)[[3]](#references)</sup>
    209 - With CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, expiry is deferred to task_work; exit_task_work runs before exit_notify, so the IRQ-time overlap with reaping does not occur.<sup>[[1]](#references)</sup>
    210 - Even then, if the task is already exiting, task_work_add() fails; gating on exit_state makes both modes consistent.<sup>[[3]](#references)</sup>
    211 
    212 Fix (Android common and Linux stable kernels) and rationale:<sup>[[1]](#references)[[3]](#references)[[6]](#references)</sup>
    213 - Add an early return if the current task is exiting, gating all processing; the stable-tree commit applies the equivalent check before running POSIX CPU timers.<sup>[[1]](#references)[[3]](#references)[[6]](#references)</sup>
    214 
    215 ```c
    216 // kernel/time/posix-cpu-timers.c (Android common kernel commit 157f357d50b5038e5eaad0b2b438f923ac40afeb)
    217 if (tsk->exit_state)
    218     return;
    219 ```
    220 
    221 - This prevents entering handle_posix_cpu_timers() for exiting tasks, eliminating the window where posix_cpu_timer_del() could miss it.cpu.firing and race with expiry processing.<sup>[[1]](#references)[[3]](#references)[[6]](#references)</sup>
    222 
    223 Impact:<sup>[[1]](#references)[[2]](#references)[[8]](#references)</sup>
    224 - Kernel memory corruption of timer structures during concurrent expiry/deletion can yield immediate crashes (DoS) and is a strong primitive toward privilege escalation due to arbitrary kernel-state manipulation opportunities.<sup>[[1]](#references)[[2]](#references)[[8]](#references)</sup>
    225 
    226 Triggering the bug (safe, reproducible conditions):<sup>[[4]](#references)</sup>
    227 Build/config:<sup>[[4]](#references)</sup>
    228 - Ensure CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n and use a kernel without the exit_state gating fix. On x86/arm64 the option is normally forced on via HAVE_POSIX_CPU_TIMERS_TASK_WORK, so researchers often patch `kernel/time/Kconfig` to expose a manual toggle:<sup>[[4]](#references)</sup>
    229 
    230 ```c
    231 config POSIX_CPU_TIMERS_TASK_WORK
    232     bool "CVE-2025-38352: POSIX CPU timers task_work toggle" if EXPERT
    233     depends on POSIX_TIMERS && HAVE_POSIX_CPU_TIMERS_TASK_WORK
    234     default y
    235 ```
    236 
    237 This mirrors what Android vendors did for analysis builds; upstream x86_64 and arm64 force HAVE_POSIX_CPU_TIMERS_TASK_WORK=y, so the vulnerable IRQ path mainly exists on 32-bit Android kernels where the option is compiled out.<sup>[[4]](#references)</sup>
    238 
    239 - Run on a multi-core VM (e.g., QEMU `-smp cores=4`) so parent, child main, and worker threads can stay pinned to dedicated CPUs.<sup>[[4]](#references)</sup>
    240 
    241 Runtime strategy:<sup>[[4]](#references)</sup>
    242 - Target a thread that is about to exit and attach a CPU timer to it (per-thread or process-wide clock):<sup>[[4]](#references)</sup>
    243   - For per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, ...)
    244   - For process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, ...)
    245 - Arm with a very short initial expiration and small interval to maximize IRQ-path entries:<sup>[[4]](#references)</sup>
    246 
    247 ```c
    248 static timer_t t;
    249 static void setup_cpu_timer(void) {
    250     struct sigevent sev = {0};
    251     sev.sigev_notify = SIGEV_SIGNAL;    // delivery type not critical for the race
    252     sev.sigev_signo = SIGUSR1;
    253     if (timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &t)) perror("timer_create");
    254     struct itimerspec its = {0};
    255     its.it_value.tv_nsec = 1;           // fire ASAP
    256     its.it_interval.tv_nsec = 1;        // re-fire
    257     if (timer_settime(t, 0, &its, NULL)) perror("timer_settime");
    258 }
    259 ```
    260 
    261 - From a sibling thread, concurrently delete the same timer while the target thread exits:<sup>[[4]](#references)</sup>
    262 
    263 ```c
    264 void *deleter(void *arg) {
    265     for (;;) (void)timer_delete(t);     // hammer delete in a loop
    266 }
    267 ```
    268 
    269 - Race amplifiers: high scheduler tick rate, CPU load, repeated thread exit/re-create cycles. The crash typically manifests when posix_cpu_timer_del() skips noticing firing due to failing task lookup/locking right after unlock_task_sighand().<sup>[[1]](#references)[[4]](#references)</sup>
    270 
    271 ### Practical PoC orchestration
    272 #### Thread & IPC choreography
    273 A reliable reproducer forks into a ptracing parent and a child that spawns the vulnerable worker thread. Two pipes (`c2p`, `p2c`) deliver the worker TID and gate each phase, while a `pthread_barrier_t` prevents the worker from arming its timer until the parent has attached. Each process or thread is pinned with `sched_setaffinity()` (e.g., parent on CPU1, child main on CPU0, worker on CPU2) to minimize scheduler noise and keep the race reproducible.<sup>[[4]](#references)[[5]](#references)</sup>
    274 
    275 #### Timer calibration with CLOCK_THREAD_CPUTIME_ID
    276 The worker arms a per-thread CPU timer so that only its own CPU consumption advances the deadline. A tunable `wait_time` (default ≈250 µs of CPU time) plus a bounded busy loop ensure that `exit_notify()` sets `EXIT_ZOMBIE` while the timer is just about to fire:<sup>[[4]](#references)[[5]](#references)</sup>
    277 
    278 <details>
    279 <summary>Minimal per-thread CPU timer skeleton</summary>
    280 
    281 ```c
    282 static timer_t timer;
    283 static long wait_time = 250000; // nanoseconds of CPU time
    284 
    285 static void timer_fire(sigval_t unused) {
    286     puts("timer fired");
    287 }
    288 
    289 static void *worker(void *arg) {
    290     struct sigevent sev = {0};
    291     sev.sigev_notify = SIGEV_THREAD;
    292     sev.sigev_notify_function = timer_fire;
    293     timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &timer);
    294 
    295     struct itimerspec ts = {
    296         .it_interval = {0, 0},
    297         .it_value    = {0, wait_time},
    298     };
    299 
    300     pthread_barrier_wait(&barrier);  // released by child main after ptrace attach
    301     timer_settime(timer, 0, &ts, NULL);
    302 
    303     for (volatile int i = 0; i < 1000000; i++); // burn CPU before exiting
    304     return NULL;                                 // do_exit() keeps burning CPU
    305 }
    306 ```
    307 
    308 </details>
    309 
    310 #### Race timeline
    311 1. Child tells the parent the worker TID via `c2p`, then blocks on the barrier.<sup>[[4]](#references)</sup>
    312 2. Parent `PTRACE_ATTACH`es, waits in `waitpid(__WALL)`, then `PTRACE_CONT` to let the worker run and exit.
    313 3. When heuristics (or manual operator input) suggest the timer was collected into the IRQ-side `firing` list, the parent executes `waitpid(tid, __WALL)` again to trigger release_task() and drop `tsk->sighand`.
    314 4. Parent signals the child over `p2c` so child main can call `timer_delete(timer)` and immediately run a helper such as `wait_for_rcu()` until the timer’s RCU callback completes.
    315 5. IRQ context eventually resumes `handle_posix_cpu_timers()` and dereferences the freed `struct k_itimer`, tripping KASAN or WARN_ON()s.<sup>[[4]](#references)</sup>
    316 
    317 #### Optional kernel instrumentation
    318 For research setups, injecting a debug-only `mdelay(500)` inside handle_posix_cpu_timers() when `tsk->comm == "SLOWME"` widens the window so the above choreography almost always wins the race. The same PoC also renames threads (`prctl(PR_SET_NAME, ...)`) so kernel logs and breakpoints confirm the expected worker is being reaped.<sup>[[4]](#references)</sup>
    319 
    320 ### Later public PoC reliability tricks
    321 Later public PoCs showed the race can also be won on unmodified 6.12.33-era kernels without inserting a debug `mdelay()`, mainly by increasing how much kernel work happens **after** `unlock_task_sighand()` and by making retries deterministic.<sup>[[7]](#references)</sup>
    322 
    323 - **Assign different jobs to different timers:** use one timer as the actual UAF target and a batch of extra "stall timers" that only exist to keep `handle_posix_cpu_timers()` busy. Public PoCs used `SIGEV_SIGNAL`/`SIGUSR1` stall timers and a distinct `SIGUSR2` signal to detect a successful timer reallocation; researchers also used unique `sigev_value` markers while instrumenting allocations.<sup>[[7]](#references)[[8]](#references)</sup>
    324 - **Calibrate on CPU time, not wall time:** measure the per-syscall cost of `clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...)` / `getpid()` and burn just enough victim CPU before `return` so `do_exit()` lands immediately before expiry. This is more stable than `nanosleep()` because the timer only advances while the victim thread actually consumes CPU.<sup>[[7]](#references)</sup>
    325 - **Stretch `complete_signal()` from userland:** queue several extra timers and create a very large thread group with `SIGUSR1` blocked in every helper thread, then drain those signals through a shared `signalfd`. When `cpu_timer_fire()` reaches `posix_timer_queue_signal()` → `send_sigqueue()` → `complete_signal()`, the kernel has to inspect far more candidate threads, stretching the post-`unlock_task_sighand()` window. Public follow-up research reported profiled handling windows around **31-34 ms** and a final exploit window around **24-26 ms** after tuning.<sup>[[7]](#references)[[8]](#references)</sup>
    326 - **Synchronize RCU-backed retries:** after `timer_delete()`, wait for an RCU grace period before attempting immediate reuse or the next retry. Public PoCs used a tiny `wait_for_rcu()` helper around `membarrier(MEMBARRIER_CMD_GLOBAL, 0)` so the freed `struct k_itimer` is no longer only pending `call_rcu()` when the next attempt starts.<sup>[[7]](#references)</sup>
    327 
    328 ### Instrumentation cues during exploitation
    329 - Add tracepoints/WARN_ONCE around unlock_task_sighand()/posix_cpu_timer_del() to spot cases where `it.cpu.firing==1` coincides with failed cpu_timer_task_rcu()/lock_task_sighand(); monitor timerqueue consistency when the victim exits.<sup>[[1]](#references)</sup>
    330 - KASAN typically reports `slab-use-after-free` inside posix_timer_queue_signal(), while non-KASAN kernels log WARN_ON_ONCE() from send_sigqueue() when the race lands, giving a quick success indicator.<sup>[[4]](#references)</sup>
    331 - Quick checks that match the public PoCs:<sup>[[4]](#references)</sup>
    332 
    333 ```bash
    334 zgrep CONFIG_POSIX_CPU_TIMERS_TASK_WORK /proc/config.gz 2>/dev/null || grep CONFIG_POSIX_CPU_TIMERS_TASK_WORK /boot/config-$(uname -r)
    335 
    336 dmesg -w | egrep 'slab-use-after-free in posix_timer_queue_signal|send_sigqueue|handle_posix_cpu_timers: delta_ns='
    337 ```
    338 
    339 Audit hotspots (for reviewers):<sup>[[1]](#references)</sup>
    340 - update_process_times() → run_posix_cpu_timers() (IRQ)
    341 - __run_posix_cpu_timers() selection (TASK_WORK vs IRQ path)
    342 - collect_timerqueue(): sets ctmr->firing and moves nodes
    343 - handle_posix_cpu_timers(): drops sighand before firing loop
    344 - posix_cpu_timer_del(): relies on it.cpu.firing to detect in-flight expiry; this check is skipped when task lookup/lock fails during exit/reap.<sup>[[1]](#references)</sup>
    345 
    346 Notes for exploitation research:<sup>[[1]](#references)[[2]](#references)[[8]](#references)</sup>
    347 - The disclosed behavior is a reliable kernel crash primitive; turning it into privilege escalation typically needs an additional controllable overlap (object lifetime or write-what-where influence) beyond the scope of this summary. Treat any PoC as potentially destabilizing and run only in emulators/VMs.<sup>[[1]](#references)[[2]](#references)[[8]](#references)</sup>
    348 
    349 ## References
    350 - [1] [Race Against Time in the Kernel’s Clockwork (StreyPaws)](https://streypaws.github.io/posts/Race-Against-Time-in-the-Kernel-Clockwork/)
    351 - [2] [Android security bulletin – September 2025](https://source.android.com/docs/security/bulletin/2025-09-01)
    352 - [3] [Android common kernel patch commit 157f357d50b5…](https://android.googlesource.com/kernel/common/+/157f357d50b5038e5eaad0b2b438f923ac40afeb%5E%21/#F0)
    353 - [4] [CVE-2025-38352 – In-the-wild Android Kernel Vulnerability Analysis and PoC](https://faith2dxy.xyz/2025-12-22/cve_2025_38352_analysis/)
    354 - [5] [poc-CVE-2025-38352 (GitHub)](https://github.com/farazsth98/poc-CVE-2025-38352)
    355 - [6] [Linux stable fix commit f90fff1e152d](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=f90fff1e152dedf52b932240ebbd670d83330eca)
    356 - [7] [CVE-2025-38352 (Part 2) - Extending The Race Window Without a Kernel Patch](https://faith2dxy.xyz/2025-12-24/cve_2025_38352_analysis_part_2/)
    357 - [8] [CVE-2025-38352 (Part 3) - Uncovering Chronomaly](https://faith2dxy.xyz/2026-01-03/cve_2025_38352_analysis_part_3/)