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

196 lines
12 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}}
Ця сторінка документує TOCTOU race condition у Linux/Android POSIX CPU timers, який може пошкодити стан таймера і призвести до падіння ядра, а за певних обставин може бути спрямований на privilege escalation.
- Затронутий компонент: kernel/time/posix-cpu-timers.c
- Примітив: expiry vs deletion race under task exit
- Залежить від конфігурації: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)
Короткий огляд внутрішньої будови (relevant for exploitation)
- Три CPU годинники відповідають за облік таймерів через cpu_clock_sample():
- CPUCLOCK_PROF: utime + stime
- CPUCLOCK_VIRT: utime only
- CPUCLOCK_SCHED: task_sched_runtime()
- Створення таймера підключає таймер до task/pid і ініціалізує 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;
}
```
- Армування вставляє в per-base timerqueue і може оновити next-expiry cache:
```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;
}
```
- Швидкий шлях уникає дорогої обробки, якщо тільки кешовані терміни не вказують на можливе спрацьовування:
```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;
}
```
- Expiration збирає прострочені таймери, позначає їх як спрацьовані, переміщує їх з черги; фактична доставка відкладається:
```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;
}
```
Два режими обробки спрацьовування
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y: спрацьовування відкладається через task_work у цільовому task
- CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: спрацьовування обробляється безпосередньо в IRQ context
```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
```
У шляху в контексті IRQ список спрацьовувань обробляється поза 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)
- Цільова задача завершується, але ще не повністю зібрана (reaped)
- Інший потік одночасно викликає posix_cpu_timer_del() для того ж таймера
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) Негайно після розблокування задача, що завершується, може бути зібрана (reaped); інший потік виконує posix_cpu_timer_del().
5) У цьому вікні posix_cpu_timer_del() може не змогти отримати стан через cpu_timer_task_rcu()/lock_task_sighand() і тому пропустити звичайну перевірку in-flight, яка дивиться timer->it.cpu.firing. Видалення відбувається так, ніби таймер не firing, пошкоджуючи стан під час обробки спрацювання, що призводить до крашів/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.
- Навіть тоді, якщо задача вже завершується, task_work_add() не виконується; перевірка exit_state робить обидва режими консистентними.
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;
```
- Це запобігає входженню в handle_posix_cpu_timers() для задач, що виходять, усуваючи вікно, у якому posix_cpu_timer_del() може пропустити it.cpu.firing і змагатися з обробкою expiry.
Impact
- Пошкодження пам'яті ядра в структурах таймера під час одночасного expiry/видалення може призвести до негайних аварій (DoS) і є потужним примітивом для ескалації привілеїв через можливості довільної маніпуляції станом ядра.
Triggering the bug (safe, reproducible conditions)
Build/config
- Переконайтеся, що CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n і використовуйте ядро без exit_state gating fix.
Runtime strategy
- Орієнтуйтеся на потік, який збирається завершитися, і прикріпіть до нього CPU timer (потоковий або процесний годинник):
- Для per-thread: timer_create(CLOCK_THREAD_CPUTIME_ID, ...)
- Для process-wide: timer_create(CLOCK_PROCESS_CPUTIME_ID, ...)
- Установіть дуже короткий початковий термін і малий інтервал, щоб максимізувати входження в IRQ-path:
```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");
}
```
- З сусіднього потоку одночасно видаліть той самий таймер, поки цільовий потік завершується:
```c
void *deleter(void *arg) {
for (;;) (void)timer_delete(t); // hammer delete in a loop
}
```
- Посилювачі гонок: висока частота тіку планувальника, навантаження CPU, повторювані цикли виходу/повторного створення потоків. Збій зазвичай проявляється, коли posix_cpu_timer_del() не помічає спрацьовування через невдалий пошук/блокування task одразу після unlock_task_sighand().
Detection and hardening
- Mitigation: застосувати exit_state guard; за можливості віддавати перевагу увімкненню CONFIG_POSIX_CPU_TIMERS_TASK_WORK.
- Observability: додати tracepoints/WARN_ONCE навколо unlock_task_sighand()/posix_cpu_timer_del(); сповіщати, коли it.cpu.firing==1 спостерігається разом з невдалим cpu_timer_task_rcu()/lock_task_sighand(); стежити за невідповідностями timerqueue під час виходу задачі.
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(): покладається на it.cpu.firing для виявлення спрацьовування в польоті; ця перевірка пропускається, коли пошук/блокування task не вдається під час виходу/збирання
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.
## 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}}