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

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

이 페이지는 Linux/Android의 POSIX CPU timers에서 발생하는 TOCTOU race 조건을 문서화하며, 이는 타이머 상태를 손상시키고 kernel을 crash시킬 수 있으며, 특정 상황에서는 privilege escalation으로 유도될 수 있습니다.

  • Affected component: kernel/time/posix-cpu-timers.c
  • Primitive: task exit 시 expiry vs deletion race
  • Config sensitive: CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n (IRQ-context expiry path)

Quick internals recap (relevant for exploitation)

  • 세 가지 CPU 클록이 cpu_clock_sample()을 통해 타이머 회계를 담당합니다:
  • CPUCLOCK_PROF: utime + stime
  • CPUCLOCK_VIRT: utime only
  • CPUCLOCK_SCHED: task_sched_runtime()
  • Timer creation wires a timer to a task/pid and initializes the timerqueue nodes:
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를 업데이트할 수 있습니다:
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는 캐시된 만료 정보가 타이머가 실제로 발동할 가능성을 시사하는 경우를 제외하고 비용이 큰 처리를 피합니다:
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은 만료된 타이머를 수집하고, 이들을 firing으로 표시한 뒤 큐에서 제거합니다; 실제 전달은 지연됩니다:
#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 컨텍스트에서 직접 처리됨
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: IRQ 시점의 만료와 태스크 종료 중 동시 삭제 사이의 TOCTOU Preconditions

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK is disabled (IRQ 경로 사용 중)
  • 대상 태스크가 종료 중이지만 완전히 회수되지 않음
  • 다른 스레드가 동일한 타이머에 대해 posix_cpu_timer_del()을(를) 동시에 호출함

Sequence

  1. update_process_times()가 IRQ 컨텍스트에서 종료 중인 태스크에 대해 run_posix_cpu_timers()를 트리거합니다.
  2. collect_timerqueue()는 ctmr->firing = 1로 설정하고 타이머를 임시 firing 리스트로 이동시킵니다.
  3. handle_posix_cpu_timers()는 unlock_task_sighand()를 통해 sighand를 해제하여 락 밖에서 타이머를 전달합니다.
  4. unlock 직후, 종료 중인 태스크가 회수될 수 있고; 형제 스레드가 posix_cpu_timer_del()를 실행합니다.
  5. 이 창에서 posix_cpu_timer_del()는 cpu_timer_task_rcu()/lock_task_sighand()를 통해 상태를 획득하지 못할 수 있으며, 따라서 timer->it.cpu.firing을 검사하는 일반적인 인-플라이트 가드를 건너뛸 수 있습니다. 삭제는 firing이 아닌 것처럼 진행되어 만료를 처리하는 동안 상태를 손상시키며, 충돌/UB로 이어집니다.

Why TASK_WORK mode is safe by design

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y인 경우, 만료는 task_work로 연기됩니다; exit_task_work는 exit_notify 이전에 실행되므로 IRQ 시점의 회수와의 겹침이 발생하지 않습니다.
  • 그 경우에도, 태스크가 이미 종료 중이면 task_work_add()는 실패합니다; exit_state에 따른 제어로 두 모드를 일관되게 만듭니다.

Fix (Android common kernel) and rationale

  • 현재 태스크가 종료 중이면 조기 리턴을 추가하여 모든 처리를 차단합니다:
// 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을 놓치고 만료 처리와 경쟁하는 윈도우를 제거합니다.

Impact

  • 동시 만료/삭제 중 타이머 구조체의 커널 메모리 손상은 즉시 크래시(DoS)를 유발할 수 있으며, 임의의 커널 상태 조작 기회 때문에 권한 상승을 위한 강력한 프리미티브가 됩니다.

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

  • CONFIG_POSIX_CPU_TIMERS_TASK_WORK=n이 설정되어 있고 exit_state gating fix가 적용되지 않은 커널을 사용하세요.

Runtime strategy

  • 종료 직전의 스레드를 대상으로 하여 CPU 타이머를 부착하세요 (스레드별 또는 프로세스 전체 시계):
  • 스레드별의 경우: timer_create(CLOCK_THREAD_CPUTIME_ID, …)
  • 프로세스 전체의 경우: timer_create(CLOCK_PROCESS_CPUTIME_ID, …)
  • 매우 짧은 초기 만료 시간과 작은 간격으로 설정하여 IRQ 경로 진입을 최대화하세요:
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");
}
  • 형제 스레드에서 대상 스레드가 종료되는 동안 동일한 타이머를 동시에 삭제:
void *deleter(void *arg) {
for (;;) (void)timer_delete(t);     // hammer delete in a loop
}
  • 레이스 증폭 요인: 높은 스케줄러 틱 속도, CPU 부하, 반복적인 스레드 종료/재생성 사이클. 크래시는 일반적으로 posix_cpu_timer_del()가 unlock_task_sighand() 직후 task 조회/잠금에 실패하여 firing을 인지하지 못할 때 발생합니다.

탐지 및 완화

  • 완화: exit_state guard를 적용하세요; 가능하면 CONFIG_POSIX_CPU_TIMERS_TASK_WORK 활성화를 우선 고려하세요.
  • 관찰성: unlock_task_sighand()/posix_cpu_timer_del() 주변에 tracepoints/WARN_ONCE를 추가하세요; it.cpu.firing==1이 cpu_timer_task_rcu()/lock_task_sighand() 실패와 함께 관측되면 경보를 발생시키세요; task 종료 주변의 timerqueue 불일치를 모니터링하세요.

감사 핵심 지점 (검토자용)

  • update_process_times() → run_posix_cpu_timers() (IRQ)
  • __run_posix_cpu_timers() 선택 (TASK_WORK vs IRQ 경로)
  • collect_timerqueue(): ctmr->firing를 설정하고 노드들을 이동
  • handle_posix_cpu_timers(): firing 루프 전에 sighand를 해제
  • posix_cpu_timer_del(): it.cpu.firing에 의존해 진행 중 만료를 감지함; exit/reap 동안 task 조회/잠금이 실패하면 이 검사가 건너뛰어짐

익스플로잇 연구 참고

  • 공개된 동작은 신뢰할 수 있는 kernel crash primitive입니다; 이를 privilege escalation으로 전환하려면 일반적으로 본 요약의 범위를 벗어나는 추가적인 제어 가능한 오버랩(객체 수명 또는 write-what-where 영향)이 필요합니다. 모든 PoC는 시스템을 불안정하게 만들 수 있으니 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: 레이싱 스레드(race_func())가 CPU를 소모하는 동안 CPU timers가 발동함; free_func()는 타이머가 발동했는지 확인하기 위해 SIGUSR1을 폴링합니다. 신호가 간헐적으로만 도착하도록 CPU_USAGE_THRESHOLD를 조정하세요(간헐적으로 “Parent raced too late/too early” 메시지가 출력되어야 함). 타이머가 항상 발동하면 threshold를 낮추고, 스레드 종료 전에 절대 발동하지 않으면 높이세요.
  • Dual-process alignment into send_sigqueue(): 부모/자식 프로세스는 send_sigqueue() 내부의 두 번째 레이스 창을 노립니다. 부모는 타이머를 설정하기 전에 PARENT_SETTIME_DELAY_US 마이크로초만큼 sleep합니다; 대부분 “Parent raced too late“가 보이면 값을 줄이고, 대부분 “Parent raced too early“가 보이면 값을 늘리세요. 둘 다 보이면 창을 건너뛰고 있는 상태입니다; 튜닝이 완료되면 약 1분 내에 성공이 기대됩니다.
  • Cross-cache UAF replacement: 익스플로잇은 struct sigqueue를 해제한 뒤 할당기 상태를 정리(sigqueue_crosscache_preallocs())하여 dangling uaf_sigqueue와 교체될 realloc_sigqueue가 pipe buffer 데이터 페이지에 같이 배치되도록 합니다(크로스-캐시 재할당). 신뢰성은 이전에 적은 수의 sigqueue 할당이 있는 조용한 커널을 가정합니다; 이미 per-CPU/per-node 부분 슬랩 페이지가 존재하는 바쁜 시스템에서는 교체가 실패하고 체인이 깨집니다. 작성자는 시끄러운 커널에 대한 최적화를 일부러 생략했습니다.

See also

Ksmbd Streams Xattr Oob Write Cve 2025 37947

References

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기