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

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:
c
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 :
c
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 :
c
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 :
c
#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
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 de contexte IRQ, la liste de déclenchement est traitée en dehors de sighand

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);
}
}

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

  1. update_process_times() déclenche run_posix_cpu_timers() en contexte IRQ pour la tùche en cours de sortie.
  2. collect_timerqueue() met ctmr->firing = 1 et déplace le timer dans la liste temporaire de firing.
  3. handle_posix_cpu_timers() libÚre sighand via unlock_task_sighand() pour délivrer les timers hors du lock.
  4. ImmĂ©diatement aprĂšs le unlock, la tĂąche en sortie peut ĂȘtre reaped ; un thread sibling exĂ©cute posix_cpu_timer_del().
  5. 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 :
c
// 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 :
c
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 :
c
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

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