197 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# POSIX CPU Timers TOCTOU race (CVE-2025-38352)
{{#include ../../../banners/hacktricks-training.md}}
Ova stranica dokumentuje TOCTOU race condition u Linux/Android POSIX CPU timerima koji može korumpirati stanje timera i srušiti kernel, a u određenim okolnostima može se iskoristiti za eskalaciju privilegija.
- Pogođena komponenta: kernel/time/posix-cpu-timers.c
- Primitiv: expiry vs deletion race under task exit
- Osetljivo na konfiguraciju: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Kratak pregled unutrašnjih mehanizama (relevantno za eksploataciju)
- Tri CPU clock-a vode obračun za timere preko cpu_clock_sample():
- CPUCLOCK_PROF: utime + stime
- CPUCLOCK_VIRT: samo utime
- CPUCLOCK_SCHED: task_sched_runtime()
- Kreiranje timera povezuje timer sa task/pid-om i inicijalizuje timerqueue čvorove:
```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;
}
```
- Arming umeće u per-base timerqueue i može ažurirati keš sledećeg isteka:
```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;
}
```
- Brza putanja izbegava skupe operacije osim ako keširani zapisi o isteku ne ukazuju na moguće okidanje:
```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;
}
```
- Isticanje prikuplja istekle tajmere, označava ih kao okinute, sklanja ih sa reda; stvarna isporuka se odlaž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;
}
```
Dva režima obrade isteka
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: istek se odlaže putem task_work na ciljanom tasku
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: istek se obrađuje direktno u IRQ kontekstu
```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
```
Na IRQ-context putanji, firing list se obrađuje izvan 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);
}
}
```
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)
- The target task is exiting but not fully reaped
- Another thread concurrently calls posix_cpu_timer_del() for the same timer
Sequence
1) update_process_times() triggers run_posix_cpu_timers() in IRQ context for the exiting task.
2) collect_timerqueue() sets ctmr->firing = 1 and moves the timer to the temporary firing list.
3) handle_posix_cpu_timers() drops sighand via unlock_task_sighand() to deliver timers outside the lock.
4) Immediately after unlock, the exiting task can be reaped; a sibling thread executes posix_cpu_timer_del().
5) In this window, posix_cpu_timer_del() may fail to acquire state via cpu_timer_task_rcu()/lock_task_sighand() and thus skip the normal in-flight guard that checks timer->it.cpu.firing. Deletion proceeds as if not firing, corrupting state while expiry is being handled, leading to crashes/UB.
Why TASK_WORK mode is safe by design
- With CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, expiry is deferred to task_work; exit_task_work runs before exit_notify, so the IRQ-time overlap with reaping does not occur.
- Even then, if the task is already exiting, task_work_add() fails; gating on exit_state makes both modes consistent.
Fix (Android common kernel) and rationale
- Add an early return if current task is exiting, gating all processing:
```c
// kernel/time/posix-cpu-timers.c (Android common kernel commit 157f357d50b5038e5eaad0b2b438f923ac40afeb)
if (tsk->exit_state)
return;
```
- Ovo sprečava ulazak u handle_posix_cpu_timers() za zadatke koji su u procesu izlaska, eliminišući prozor u kome posix_cpu_timer_del() može da propusti it.cpu.firing i da se trka sa obradom isteka.
Uticaj
- Oštećenje kernel memorije u strukturama timera tokom istovremenog isteka/brisanja može dovesti do trenutnih padova (DoS) i predstavlja snažan primitiv za eskalaciju privilegija zbog mogućnosti proizvoljne manipulacije stanjem kernela.
Pokretanje buga (bezbedni, ponovljivi uslovi)
Build/config
- Obezbedite CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n i koristite kernel bez fix-a za exit_state gating.
Strategija izvršavanja (runtime)
- Ciljajte nit koja je na izlasku i pridružite joj CPU timer (po niti ili za ceo proces):
- For per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, ...)
- For process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, ...)
- Podesite veoma kratko početno isteknuće i mali interval kako biste maksimalizovali ulaze u IRQ-putanju:
```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");
}
```
- Iz srodnog thread-a, istovremeno obrišite isti timer dok ciljni thread izlazi:
```c
void *deleter(void *arg) {
for (;;) (void)timer_delete(t); // hammer delete in a loop
}
```
- Pojačivači trke: visok scheduler tick rate, CPU opterećenje, ponovljeni ciklusi izlaska/ponovnog kreiranja thread-ova. Crash se obično manifestuje kada posix_cpu_timer_del() preskoči uočavanje firing-a zbog neuspeha pri task lookup/locking odmah nakon unlock_task_sighand().
Detection and hardening
- Mitigation: primeniti exit_state guard; po mogućstvu omogućiti CONFIG_POSIX_CPU_TIMERS_TASK_WORK.
- Observability: dodati tracepoints/WARN_ONCE oko unlock_task_sighand()/posix_cpu_timer_del(); alarmirati kada se it.cpu.firing==1 uoči zajedno sa neuspehom cpu_timer_task_rcu()/lock_task_sighand(); pratiti neusaglašenosti u timerqueue oko izlaska task-a.
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
- Objavljeno ponašanje predstavlja pouzdan kernel crash primitive; da bi se to pretvorilo u privilege escalation obično je potreban dodatni kontrolisani overlap (object lifetime ili write-what-where uticaj) izvan opsega ovog sažetka. Smatrajte svaki PoC potencijalno destabilizujućim i pokrećite ga samo u emulatorima/VMs.
## References
- [Race Against Time in the Kernels 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)
{{#include ../../../banners/hacktricks-training.md}}