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

Cette page documente une condition de course TOCTOU dans les POSIX CPU timers de Linux/Android qui peut corrompre l’état des timers et provoquer un crash du noyau, et qui, dans certaines circonstances, peut être orientée vers une privilege escalation.

  • Affected component: kernel/time/posix-cpu-timers.c
  • Primitive : course entre expiration et suppression lors de la sortie d’une tâche
  • Dépendant de la configuration : CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)

Rappel rapide des internals (pertinent pour l’exploitation)

  • Trois horloges CPU alimentent la comptabilisation 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 un timer à une tâche/pid et initialise les nœuds de la 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 per-base timerqueue et peut mettre à jour le next-expiry cache:
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;
}
  • Expiration collecte les timers expirés, les marque comme firing, les déplace hors de la queue ; 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
Chemins d'exécution des timers CPU POSIX ```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 d’exécution en contexte IRQ, la liste des déclenchements est traitée en dehors de sighand

Chemin de traitement 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); } } ```

Cause racine : TOCTOU entre l’expiration en IRQ-time et la suppression concurrente lors de la sortie d’une tâche Prérequis

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK est désactivé (chemin IRQ en usage)
  • La tâche cible est en train de sortir mais n’est pas complètement récupérée
  • Un autre thread appelle concurremment 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 sortie.
  2. collect_timerqueue() définit ctmr->firing = 1 et déplace le timer vers la liste temporaire des timers en cours de déclenchement.
  3. handle_posix_cpu_timers() relâche sighand via unlock_task_sighand() pour traiter les timers hors du verrou.
  4. Immédiatement après l’unlock, la tâche en sortie peut être récupérée ; 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 ignorer la garde normale en vol qui vérifie timer->it.cpu.firing. La suppression se poursuit comme si le timer ne déclenchait pas, corrompant l’état pendant le traitement de l’expiration, entraînant 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 vers task_work ; exit_task_work s’exécute avant exit_notify, donc le chevauchement IRQ-time avec la récupération ne se produit pas.
  • Même alors, si la tâche est déjà en train de sortir, task_work_add() échoue ; conditionner sur exit_state rend les deux modes cohérents.

Fix (Android common kernel) et justification

  • Ajouter un retour anticipé si la tâche courante est en train de quitter, 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 d’arrêt, éliminant la fenêtre où posix_cpu_timer_del() pourrait rater it.cpu.firing et entrer en concurrence avec le traitement d’expiration.

Impact

  • La corruption de mémoire du noyau des structures de timer lors d’expirations/suppressions concurrentes peut provoquer des plantages immédiats (DoS) et constitue un puissant vecteur vers une élévation de privilèges en raison des possibilité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.

Runtime strategy

  • Ciblez un thread sur le point de se terminer et attachez-lui un CPU timer (horloge par thread ou globale au processus) :
  • For per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, …)
  • For process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, …)
  • Armez-le avec une expiration initiale très courte et un petit intervalle pour maximiser les entrées du 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");
}
  • À partir d’un sibling thread, supprimer simultanément le même timer pendant que le target thread se termine :
void *deleter(void *arg) {
for (;;) (void)timer_delete(t);     // hammer delete in a loop
}
  • Amplificateurs de race : high scheduler tick rate, CPU load, repeated thread exit/re-create cycles. Le plantage se manifeste typiquement lorsque posix_cpu_timer_del() omet de détecter un firing parce que la recherche/verrouillage de la task échoue juste après unlock_task_sighand().

Détection et durcissement

  • Mitigation : appliquer le guard exit_state ; 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 lorsqu’on observe it.cpu.firing==1 conjointement avec des échecs de cpu_timer_task_rcu()/lock_task_sighand() ; surveiller les incohérences du timerqueue autour de la sortie de la task.

Points chauds d’audit (pour les reviewers)

  • update_process_times() → run_posix_cpu_timers() (IRQ)
  • __run_posix_cpu_timers() selection (TASK_WORK vs IRQ path)
  • collect_timerqueue() : met ctmr->firing et déplace des nœuds
  • handle_posix_cpu_timers() : droppe sighand avant la boucle de firing
  • posix_cpu_timer_del() : s’appuie sur it.cpu.firing pour détecter une expiry en vol ; cette vérification est contournée quand la recherche/verrouillage de task échoue durant exit/reap

Notes pour la recherche en exploitation

  • Le comportement divulgué est une primitive fiable de crash du kernel ; en faire une escalation de privilèges demande typiquement un chevauchement contrôlable supplémentaire (durée de vie d’un 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.

Chronomaly exploit strategy (priv-esc without fixed text offsets)

  • Tested target & configs: x86_64 v5.10.157 under QEMU (4 cores, 3 GB RAM). Critical options: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n, CONFIG_PREEMPT=y, CONFIG_SLAB_MERGE_DEFAULT=n, DEBUG_LIST=n, BUG_ON_DATA_CORRUPTION=n, LIST_HARDENED=n.
  • Race steering with CPU timers: Un thread en race (race_func()) consomme du CPU pendant que les CPU timers déclenchent ; free_func() poll SIGUSR1 pour confirmer si le timer a fired. Ajustez CPU_USAGE_THRESHOLD pour que les signaux n’arrivent que parfois (messages intermittents “Parent raced too late/too early”). Si les timers déclenchent à chaque tentative, baissez le threshold ; s’ils ne déclenchent jamais avant la sortie du thread, augmentez-le.
  • Dual-process alignment into send_sigqueue(): Les processus parent/enfant tentent d’atteindre une seconde fenêtre de race à l’intérieur de send_sigqueue(). Le parent sleep PARENT_SETTIME_DELAY_US microsecondes avant d’armer les timers ; ajustez à la baisse si vous voyez surtout “Parent raced too late” et à la hausse si vous voyez surtout “Parent raced too early”. Voir les deux indique que vous chevauchez la fenêtre ; le succès est attendu en ~1 minute une fois réglé.
  • Cross-cache UAF replacement: L’exploit free un struct sigqueue puis groom l’état de l’allocateur (sigqueue_crosscache_preallocs()) pour que le uaf_sigqueue (dangling) et le realloc_sigqueue de remplacement se retrouvent tous deux sur une page de données de pipe buffer (reallocation cross-cache). La fiabilité suppose un kernel tranquille avec peu d’allocations sigqueue préalables ; si des pages de slab partielles par CPU/par node existent déjà (systèmes occupés), le remplacement manquera et la chaîne échouera. L’auteur l’a volontairement laissé non optimisé pour des kernels bruyants.

See also

Ksmbd Streams Xattr Oob Write Cve 2025 37947

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