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

Tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

На цій сторінці описано TOCTOU race condition у Linux/Android POSIX CPU timers, яка може пошкодити стан таймера та викликати крах ядра, і за певних обставин може призвести до privilege escalation.

  • Затронутий компонент: kernel/time/posix-cpu-timers.c
  • Примітив: expiry vs deletion race під час task exit
  • Чутливий до конфігурації: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)

Коротке пояснення внутрішньої роботи (relevant for exploitation)

  • Три CPU clocks відповідають за облік таймерів через cpu_clock_sample():
  • CPUCLOCK_PROF: utime + stime
  • CPUCLOCK_VIRT: utime only
  • CPUCLOCK_SCHED: task_sched_runtime()
  • Створення таймера прив’язує timer до task/pid і ініціалізує вузли 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;
}
  • Активування вставляє в per-base timerqueue і може оновити 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;
}
  • Швидкий шлях уникає дорогої обробки, якщо кешовані строки спрацювання не вказують на можливу активацію:
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 збирає прострочені таймери, позначає їх як спрацьовані, переміщує їх із черги; фактична доставка відкладена:
#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-контексті
POSIX CPU timer шляхи виконання ```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-context firing list обробляється поза sighand

Шлях обробки 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 Передумови

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (використовується IRQ-шлях)
  • Цільове завдання виходить, але ще не повністю зібране (reaped)
  • Інший потік одночасно викликає posix_cpu_timer_del() для того самого таймера

Послідовність

  1. update_process_times() викликає run_posix_cpu_timers() в IRQ-контексті для завдання, що виходить.
  2. collect_timerqueue() встановлює ctmr->firing = 1 і переміщує таймер у тимчасовий список firing.
  3. handle_posix_cpu_timers() звільняє sighand через unlock_task_sighand(), щоб доставляти таймери поза локом.
  4. Негайно після unlock, завдання, що виходить, може бути reaped; суміжний потік виконує posix_cpu_timer_del().
  5. У цьому проміжку posix_cpu_timer_del() може не змогти отримати стан через cpu_timer_task_rcu()/lock_task_sighand() і тому пропустити звичайний in-flight захист, що перевіряє timer->it.cpu.firing. Видалення продовжується так, ніби таймер не firing, ушкоджуючи стан під час обробки expiry, що призводить до крашів/UB.

Чому режим TASK_WORK за своєю конструкцією безпечний

  • З CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, expiry відкладається до task_work; exit_task_work виконується перед exit_notify, тому накладання IRQ-часу з reaping не відбувається.
  • Навіть тоді, якщо завдання вже виходить, task_work_add() зазнає невдачі; перевірка exit_state робить обидва режими узгодженими.

Fix (Android common kernel) and rationale

  • Додати ранній вихід, якщо current task виходить, блокуючи всю обробку:
// 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 processing).

Impact

  • Пошкодження пам’яті ядра у структурах таймерів під час одночасного закінчення/видалення може викликати негайні аварії (DoS) і є потужним примітивом для підвищення привілеїв через можливості довільної маніпуляції станом ядра.

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

  • Переконайтесь, що CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n і використовуйте ядро без виправлення exit_state gating.

Runtime strategy

  • Ціль — потік, який збирається завершитись, і прикріпіть до нього 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, …)
  • 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");
}
  • З суміжного потоку одночасно видаліть той самий timer, поки цільовий потік завершується:
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

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks