POSIX CPU Timers TOCTOU race (CVE-2025-38352)

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Esta página documenta una TOCTOU race condition en Linux/Android POSIX CPU timers que puede corromper el estado del timer y provocar un crash del kernel, y bajo algunas circunstancias poder dirigirse hacia privilege escalation.

  • Componente afectado: kernel/time/posix-cpu-timers.c
  • Primitiva: condición de carrera de expiry vs deletion durante la salida de la tarea
  • Dependiente de la configuración: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (ruta de expiry en contexto IRQ)

Breve repaso interno (relevante para la explotación)

  • Tres relojes de CPU manejan la contabilización para los timers vía cpu_clock_sample():
  • CPUCLOCK_PROF: utime + stime
  • CPUCLOCK_VIRT: utime únicamente
  • CPUCLOCK_SCHED: task_sched_runtime()
  • La creación del timer vincula un timer a una tarea/pid e inicializa los nodos 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;
}
  • El armado inserta en una timerqueue por base y puede actualizar la caché de próxima expiración:
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;
}
  • La ruta rápida evita el procesamiento costoso a menos que las expiraciones en caché indiquen una posible activación:
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;
}
  • La expiración recopila temporizadores expirados, los marca como disparados, los mueve fuera de la cola; la entrega real se pospone:
#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;
}

Dos modos de procesamiento de expiración

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: la expiración se difiere mediante task_work en la tarea objetivo
  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: la expiración se maneja directamente en el contexto de IRQ
Rutas de ejecución de los temporizadores 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 ```

En la ruta IRQ-context, la lista de activación se procesa fuera de sighand

Ruta de manejo en IRQ-context ```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 between IRQ-time expiry and concurrent deletion under task exit Preconditions

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ path in use)
  • La tarea objetivo está saliendo pero no ha sido completamente eliminada
  • Otro hilo llama concurrentemente a posix_cpu_timer_del() para el mismo timer

Sequence

  1. update_process_times() desencadena run_posix_cpu_timers() en contexto IRQ para la tarea que está saliendo.
  2. collect_timerqueue() establece ctmr->firing = 1 y mueve el timer a la lista temporal de firing.
  3. handle_posix_cpu_timers() libera sighand mediante unlock_task_sighand() para entregar timers fuera del lock.
  4. Inmediatamente después del unlock, la tarea que sale puede ser reaped; un hilo hermano ejecuta posix_cpu_timer_del().
  5. En esta ventana, posix_cpu_timer_del() puede fallar al adquirir el state vía cpu_timer_task_rcu()/lock_task_sighand() y por tanto omitir la protección normal in-flight que comprueba timer->it.cpu.firing. La eliminación procede como si no estuviera firing, corrompiendo el estado mientras se maneja la expiración, lo que conduce a crashes/UB.

Why TASK_WORK mode is safe by design

  • Con CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, la expiración se difiere a task_work; exit_task_work se ejecuta antes de exit_notify, por lo que no ocurre la superposición en tiempo de IRQ con el reaping.
  • Aún así, si la tarea ya está saliendo, task_work_add() falla; condicionar con exit_state hace que ambos modos sean consistentes.

Fix (Android common kernel) and rationale

  • Agregar un return temprano si la tarea actual está saliendo, condicionando todo el procesamiento:
// kernel/time/posix-cpu-timers.c (Android common kernel commit 157f357d50b5038e5eaad0b2b438f923ac40afeb)
if (tsk->exit_state)
return;
  • Esto evita entrar en handle_posix_cpu_timers() para tareas que están saliendo, eliminando la ventana donde posix_cpu_timer_del() podría perder it.cpu.firing y competir con el procesamiento de expiry.

Impacto

  • La corrupción de memoria del kernel de las estructuras de temporizador durante la expiración/eliminación concurrente puede provocar bloqueos inmediatos (DoS) y constituye un potente primitivo para escalada de privilegios debido a las oportunidades de manipular arbitrariamente el estado del kernel.

Triggering the bug (safe, reproducible conditions) Build/config

  • Asegúrate de que CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n y usa un kernel sin la corrección de gating de exit_state.

Runtime strategy

  • Apunta a un hilo que esté a punto de salir y adjúntale un CPU timer (reloj por hilo o a nivel de proceso):
  • Por hilo: timer_create(CLOCK_THREAD_CPUTIME_ID, …)
  • A nivel de proceso: timer_create(CLOCK_PROCESS_CPUTIME_ID, …)
  • Arm with a very short initial expiration and small interval to maximize IRQ-path entries:
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");
}
  • Desde un hilo hermano, eliminar concurrentemente el mismo timer mientras el hilo objetivo termina:
void *deleter(void *arg) {
for (;;) (void)timer_delete(t);     // hammer delete in a loop
}
  • Race amplifiers: high scheduler tick rate, CPU load, repeated thread exit/re-create cycles. The crash typically manifests when posix_cpu_timer_del() skips noticing firing due to failing task lookup/locking right after unlock_task_sighand().

Detection and hardening

  • Mitigation: apply the exit_state guard; prefer enabling CONFIG_POSIX_CPU_TIMERS_TASK_WORK when feasible.
  • Observability: add tracepoints/WARN_ONCE around unlock_task_sighand()/posix_cpu_timer_del(); alert when it.cpu.firing==1 is observed together with failed cpu_timer_task_rcu()/lock_task_sighand(); watch for timerqueue inconsistencies around task exit.

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

  • The disclosed behavior is a reliable kernel crash primitive; turning it into privilege escalation typically needs an additional controllable overlap (object lifetime or write-what-where influence) beyond the scope of this summary. Treat any PoC as potentially destabilizing and run only in emulators/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: A racing thread (race_func()) burns CPU while CPU timers fire; free_func() polls SIGUSR1 to confirm if the timer fired. Tune CPU_USAGE_THRESHOLD so signals arrive only sometimes (intermittent “Parent raced too late/too early” messages). If timers fire every attempt, lower the threshold; if they never fire before thread exit, raise it.
  • Dual-process alignment into send_sigqueue(): Parent/child processes try to hit a second race window inside send_sigqueue(). The parent sleeps PARENT_SETTIME_DELAY_US microseconds before arming timers; adjust downward when you mostly see “Parent raced too late” and upward when you mostly see “Parent raced too early”. Seeing both indicates you are straddling the window; success is expected within ~1 minute once tuned.
  • Cross-cache UAF replacement: The exploit frees a struct sigqueue then grooms allocator state (sigqueue_crosscache_preallocs()) so both the dangling uaf_sigqueue and the replacement realloc_sigqueue land on a pipe buffer data page (cross-cache reallocation). Reliability assumes a quiet kernel with few prior sigqueue allocations; if per-CPU/per-node partial slab pages already exist (busy systems), the replacement will miss and the chain fails. The author intentionally left it unoptimized for noisy kernels.

See also

Ksmbd Streams Xattr Oob Write Cve 2025 37947

References

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks