摘要

此研究報告針對 GhostLock (CVE-2026-43499) 提供全面的技術分析,這是一個存在於 Linux kernel real-time mutex (rtmutex) 優先權繼承子系統中的 Stack-Based Use-After-Free(UAF) 漏洞。該漏洞自 Linux 2.6.39 引入,直到在 7.1 版中被修復,持續了超過十五年,允許無特權的本機攻擊者取得指向 Stack 記憶體的 dangling kernel pointer,達成受限的任意記憶體寫入,並最終將權限提升至 root。漏洞攻擊鏈利用了 FUTEX_CMP_REQUEUE_PI 系統呼叫路徑,其中一個清理輔助函式錯誤地操作了當前執行的任務,而非代理的等待任務,導致過時的優先權繼承狀態參照到已釋放的 Stack 框架。報告探討根本原因、分析漏洞利用基本操作、討論透過 PR_SET_MM_MAP 進行的 Stack 回收技術,並評估緩解策略。
十五年無人察覺!GhostLock 在 Linux 核心 rtmutex 中埋下的 Stack-UAF 幽靈! | 資訊安全新聞

1. 簡介

Linux kernel 的 real-time mutex(rtmutex) 子系統實作了優先權繼承 (Priority Inheritance, PI),以防止多執行緒環境中的優先權反轉(Priority inversion)情境 [1] 。當高優先權任務因被低優先權任務 Lock 而受阻時,PI 會暫時提升 lock holder 的排程優先權,以確保資源能及時釋放。FUTEX_WAIT_REQUEUE_PI 與 FUTEX_CMP_REQUEUE_PI 機制將此典範延伸至使用者空間的 futex 操作,允許執行緒原子地等待(Atomically wait)一個 futex,並被重新排隊到一個優先權繼承的 futex [2]

GhostLock 代表 rtmutex 內部代理鎖定機制清理路徑中的一個嚴重缺陷。此漏洞是在提交 8161239a8bcc 進行 rtmutex 重構時引入,並在約十五年間未被發現,影響了所有主要的 Linux 發行版 [1] 。此錯誤僅需 CONFIG_FUTEX_PI=y,且不需要特殊權限或使用者命名空間能力,使其在受影響的系統上普遍可利用。

先前關於 Linux kernel Use-After-Free 漏洞利用的研究主要集中在 Heap-Based 的漏洞,像是 Heap Feng Shui、Cross-Cache 攻擊和 Physmap Spraying 等技術主導了該領域 [3] [4] 。Kernel 中的 Stack-Based UAF 漏洞受到的關注相對較少,部分原因是 Kernel Stack 框架通常生命週期短暫且難以預測地回收。GhostLock 證明了當結合能將大型受控緩衝區放置在可預測 Stack 深度的系統呼叫時,Stack UAF 的可利用性不亞於 Heap UAF。

2. 漏洞分析

2.1 根本原因:remove_waiter() 中錯誤的任務參照

Kernel漏洞存在於 kernel/locking/rtmutex.c 中的 remove_waiter() 輔助函式。此函式最初是為單一情境設計:一個執行緒代表自己 block,隨後自行清理。在此假設下,current 巨集(識別當前執行的任務)總是對應到擁有正在 dequeue 的 waiter 物件的任務 [1]

然而,Requeue-PI 代理路徑(proxy path)打破了這個基本假設。透過 rt_mutex_start_proxy_lock(),remove_waiter() 輔助函式被呼叫來代表另一個正在睡眠的執行緒進行清理。在此代理路徑上,current 指的是發出 FUTEX_CMP_REQUEUE_PI 的執行緒,而非實際的等待任務。當 __rt_mutex_start_proxy_lock() 因偵測到 deadlock cycle 而回傳 -EDEADLK 時,復原路徑會呼叫 remove_waiter(),然後該函式錯誤地清除 current->pi_blocked_on,而非 waiter->task->pi_blocked_on [1]

以下程式碼片段說明了此有漏洞的代理鎖定啟動路徑:

  1. // Code from kernel/locking/rtmutex.c (vulnerable version)
  2. // This function initiates proxy locking on behalf of 'task',
  3. // not the currently executing task.
  4. int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock,
  5. struct rt_mutex_waiter *waiter,
  6. struct task_struct *task)
  7. {
  8. int ret;
  9. raw_spin_lock_irq(&lock->wait_lock);
  10. ret = __rt_mutex_start_proxy_lock(lock, waiter, task);
  11. // When ret == -EDEADLK (deadlock detected), remove_waiter is called
  12. // to roll back the operation. However, remove_waiter assumes 'current'
  13. // is the waiter task, which is FALSE on the proxy path.
  14. if (unlikely(ret))
  15. remove_waiter(lock, waiter); // BUG: clears wrong task's pi_blocked_on
  16. raw_spin_unlock_irq(&lock->wait_lock);
  17. return ret;
  18. }

接著 remove_waiter() 函式執行了錯誤的清理:

  1. // Vulnerable remove_waiter() implementation
  2. // The function locks current->pi_lock and clears current->pi_blocked_on.
  3. // On the proxy path, 'current' is the REQUEUER, not the WAITER.
  4. static void __sched remove_waiter(struct rt_mutex_base *lock,
  5. struct rt_mutex_waiter *waiter)
  6. {
  7. ...
  8. raw_spin_lock(&current->pi_lock); // Locks WRONG task's pi_lock
  9. rt_mutex_dequeue(lock, waiter); // Dequeues waiter from lock tree
  10. current->pi_blocked_on = NULL; // Clears WRONG task's pi_blocked_on
  11. // Should be: waiter->task->pi_blocked_on
  12. raw_spin_unlock(&current->pi_lock);
  13. ...
  14. }

結果是睡眠中的等待任務保留著 pi_blocked_on,指向其自身 Stack 上的 rt_mutex_waiter 物件。當等待任務返回使用者空間時,該 Stack 框架被彈出——在邏輯意義上被釋放——然而 Kernel透過任務的 pi_blocked_on 欄位保留了一個 Dangling pointer [1]

2.2 觸發 Deadlock 路徑

要觸發 -EDEADLK 復原,需要使用三個 futex words 和三個執行緒來建構一個 PI 依賴循環(dependency cycle)。其順序如下 [1]

(1) 等待者 (waiter) 執行緒取得 f_pi_chain(一個 PI futex),然後在 FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) 中 block。其 rt_mutex_waiter 物件現在配置於其 Kernel Stack 上。

(2) 持有者 (owner) 執行緒取得 f_pi_target(Requeue 的目標 PI futex),然後在 f_pi_chain 上 block ,而這個 chain 由等待者持有。

(3) 主 (main) 執行緒呼叫 FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)。

Requeue 操作嘗試將等待者代理到 f_pi_target 上。f_pi_target 的持有者已經透過 f_pi_chain block 在等待者之後,因此 PI chain 進入閉環:等待者 -> f_pi_target -> 持有者 -> f_pi_chain -> 等待者。這個 chain 被偵測到此循環並回傳 -EDEADLK,觸發 remove_waiter() 中有錯誤的復原 [1]

復原之後,等待任務被喚醒並返回使用者空間,但其 pi_blocked_on 仍指向現已釋放的 Stack 框架。UAF window 實際上是無期限的——等待者可以停留在使用者空間,而 Dangling pointer 則持續存在於 Kernel狀態中。

sequenceDiagram participant W as Waiter Thread participant O as Owner Thread participant M as Main Thread participant K as Kernel (rtmutex) Note over W,M: Stage 1: Setup W->>K: lock(f_pi_chain) W->>K: FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) Note right of W: rt_mutex_waiter allocated on W's stack O->>K: lock(f_pi_target) O->>K: lock(f_pi_chain) [blocks] Note right of O: Owner blocked on f_pi_chain Note over W,M: Stage 2: Trigger Requeue M->>K: FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target) K->>K: PI chain walk detects cycle Note right of K: waiter->f_pi_target->owner->f_pi_chain->waiter K->>K: Returns -EDEADLK Note over W,M: Stage 3: Buggy Rollback K->>K: remove_waiter(lock, waiter) Note right of K: BUG: clears current->pi_blocked_on
(current = Main Thread, not Waiter) K->>W: Wakeup W->>W: Return to userspace Note right of W: Stack frame popped,
but pi_blocked_on still points to freed stack Note over W,M: Stage 4: UAF Trigger M->>K: sched_setattr() on Waiter K->>K: PI chain walk dereferences dangling pi_blocked_on Note right of K: Use-After-Free on kernel stack

3. 漏洞利用操作與 Stack 回收

3.1 Dangling pointer 操作

一旦三個 futex 的循環被建構好,等待任務就持有一個指向其舊有 FUTEX_WAIT_REQUEUE_PI Stack 框架的 Dangling Kernel 指標。任何透過此任務觸發 PI chain 走訪的操作——例如 sched_setattr()——都會將 pi_blocked_on 作為 rt_mutex_waiter 指標進行 dereference [1] 。這為攻擊者提供了一條進入已釋放 Stack 記憶體的受控 dereference 路徑。

rt_mutex_waiter 結構定義如下:

  1. // Structure definition from kernel/locking/rtmutex.c
  2. // This object lives on the kernel stack during FUTEX_WAIT_REQUEUE_PI
  3. struct rt_mutex_waiter {
  4. struct rt_waiter_node tree; // rb node, embedded in lock->waiters tree
  5. struct rt_waiter_node pi_tree; // rb node in task's pi_waiters tree
  6. struct task_struct *task; // Pointer to the waiting task
  7. struct rt_mutex_base *lock; // Pointer to the lock being waited on
  8. unsigned int wake_state; // Wakeup state for the waiter
  9. struct ww_acquire_ctx *ww_ctx; // Wound-wait context (NULL for PI futex)
  10. };

3.2 透過 PR_SET_MM_MAP 進行 Stack 回收

利用 Stack UAF 的關鍵挑戰在於,在 Dangling pointer 被 dereference 之前,要以受控資料回收已釋放的 Stack 框架。GhostLock 漏洞利用透過 prctl(PR_SET_MM, PR_SET_MM_MAP, ...) 來實現,此呼叫會將使用者提供的輔助向量 (auxiliary vector, auxv) 複製到 prctl_set_mm_map() 內部一個固定大小的 unsigned long user_auxv[AT_VECTOR_SIZE] Stack 緩衝區中 [1]

此緩衝區大約位於與已釋放之 rt_mutex_waiter 框架相同的 Stack 深度,提供了一個大型、自然對齊、無命名空間干擾的受控 qword 區塊,直接覆蓋了舊物件。auxv 的佈局經過精心設計,使得重疊的 qword 變為:

- tree :一個精心製作的 rb 節點,使得在刪除它時會將一個選定的子節點指標 (W0_BASE) 提升為樹根。

- task :設定為 &init_task,一個有效的 task_struct,以確保 chain 走訪能安全地進行 dereference。

- lock :設定為 &inet6_protos[IPPROTO_UDP] - 8,即受限制的寫入目標。

- wake_state :設定為 0。

此漏洞利用引入了一個 Race condition,方法是將 auxv 備份在一個 memfd 中,並將複製位置設定在跨越頁面邊界之處。一個 sibling thread 在 prctl 期間對後續頁面進行 fallocate(PUNCH_HOLE) 競爭,拉長了 copy_from_user window,確保偽造的 waiter 在 consumer thread 於另一個 CPU 上觸發 sched_setattr() 時,仍然存活在 Stack 上 [1]

3.3 從偽造 Waiter 到受限制寫入

控制偽造的 waiter 並不能直接產生任意寫入能力。PI chain 走訪會執行以下順序 [1]

  1. // The chain walk dereferences task->pi_blocked_on (our fake waiter),
  2. // then accesses fake_waiter->lock (our target pointer).
  3. // rt_mutex_dequeue() performs rb_erase on lock->waiters.
  4. task->pi_blocked_on -> fake waiter
  5. fake waiter->lock -> fake rt_mutex_base
  6. rt_mutex_dequeue(lock, waiter) // rb_erase on lock->waiters

rt_mutex_dequeue() 操作是一個 rb-tree 刪除。當刪除一個只有單一子節點的根節點時, Kernel會將該子節點指標寫入根節點位置 (rb_root.rb_node)。透過將 lock 指向 target - 8,rt_mutex_base 的欄位會對齊在目標指標周圍的資料上:

  1. // Memory layout at the fake lock target:
  2. // target - 8 -> raw_spinlock_t wait_lock (must read as "unlocked")
  3. // target -> waiters.rb_root.rb_node (this slot gets WRITTEN)
  4. // target + 8 -> waiters.rb_leftmost
  5. // target + 16 -> owner
  6. // The write primitive is a single constrained store:
  7. *(uint64_t *)target = W0_BASE;

限制條件很嚴格:目標之前的 qword 必須讀起來像是一個未鎖定的 spinlock(低 4 位元組為零),且後續的 qword 不能將走訪導向不受控的記憶體 [1] 。選擇的寫入目標是 inet6_protos[IPPROTO_UDP],這是一個可寫入 Kernel資料中的函式指標表,其相鄰進入點(entry)自然能滿足這些限制。

4. 控制流劫持與權限提升

4.1 透過 Prefetch 時序繞過 KASLR

在利用受限制寫入之前,攻擊者必須先擊敗核心位址空間佈局隨機化 (Kernel Address Space Layout Randomization, KASLR)。此漏洞利用採用利用 Prefetch 的 side-channel 技術,測量已對應與未對應位址之間的循環差異 [1] 。由於 Linux 對 Kernel image base 採用相對較弱的隨機化(x86 上約 9 位元的熵),透過多次測量取平均,可以近乎 100% 的可靠度還原出 KASLR 偏移。

CPU Entry Area (CEA) 提供了第二個關鍵操作。在 Kernel 6.2 之前,CEA 位於一個完全固定的虛擬位址。在引入強隨機化之後,CEA 的 direct-map 別名仍然可預測,因為其實體偏移是固定的。direct-map 位址可從 physmap base 推算,而 physmap base 同樣可透過 prefetch 時序洩漏 [1]

  1. // CEA direct-map alias computation
  2. // physmap_base is leaked via prefetch side-channel
  3. // CPU1_CEA_BASE is a fixed physical offset depending on kernel version
  4. // For kernelCTF LTS 6.12.80 with 3.5G boot: 0x11c517000(+0x1f58)
  5. cea_direct = physmap_base + CPU1_CEA_BASE

4.2 函式表覆寫

受限制的寫入目標是 inet6_protos[IPPROTO_UDP],這是 IPv6 協定堆疊中的一個函式指標表。此目標周圍的佈局自然能滿足 spinlock 與 rb-tree 的限制 [1]

  1. // inet6_protos array layout (indices shown):
  2. inet6_protos[16] == NULL // fake wait_lock -> reads as unlocked
  3. inet6_protos[17] == &udpv6_protocol // <- target (IPPROTO_UDP = 17)
  4. inet6_protos[18] == NULL // fake rb_leftmost
  5. inet6_protos[19] == NULL // fake owner

寫入之後,inet6_protos[IPPROTO_UDP] 指向 CEA 頁面。 Kernel期望在此位置有一個 inet6_protocol 結構:

  1. // Expected structure at the overwritten pointer:
  2. struct inet6_protocol {
  3. int (*handler)(struct sk_buff *skb); // First pivot gadget address
  4. int (*err_handler)(...); // Unused
  5. unsigned int flags; // INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
  6. };

觸發一個 loopback IPv6 UDP 封包(先 connect 再寫入 ::1)會導致 Kernel dereference handler 指標,將控制權轉移到攻擊者選擇的程式碼 [1]

4.3 DirtyMode:用於權限翻轉的最小 ROP

在 kernelCTF 目標 (lts-6.12.80) 上,沒有方便的單指令 Stack Pivot 可用。漏洞攻擊鏈多執行一次載入/呼叫,將 CEA 位址送入 rbp,接著執行 mov rsp, rbp; pop rbp; ret 來 pivot 到位於 CEA 中的 ROP Stack [1]

漏洞利用沒有構建一個用於完整任意寫入的冗長 ROP 鏈,而是採用了 DirtyMode:一個單一寫入操作,翻轉 core_pattern sysctl 的 mode 欄位權限位元。coredump_sysctls 表格位於可寫入的 Kernel資料中,並與 Kernel映像共用相同的 KASLR 偏移 [1]

  1. // Target structure in kernel writable data:
  2. static struct ctl_table coredump_sysctls[] = {
  3. ...
  4. { .procname = "core_pattern",
  5. .data = core_pattern,
  6. .maxlen = CORENAME_MAX_SIZE,
  7. .mode = 0644, // <- Permission bits to flip
  8. .proc_handler = proc_dostring_coredump },
  9. ...
  10. };

一個簡短的 ROP 序列將一個寬鬆的值寫入 coredump_sysctls [1] .mode,使 /proc/sys/kernel/core_pattern 變成 world-writable。被劫持的執行緒隨後透過 msleep 安全地停放。無特權的攻擊者開啟 core_pattern,寫入 |/proc/%P/fd/666 %P,然後使一個輔助處理程序 crash,以 root 身分執行任意程式碼 [1]

5. 緩解措施與修補程式分析

5.1 官方修補程式

修補方案提交為 3bfdc63936dd,修改了 remove_waiter(),使其透過 waiter->task 而非 current 來正確識別等待任務。此修補程式同時更新了 rt_mutex_adjust_prio_chain(),以傳遞正確的任務來進行優先權鏈調整 [1]

  1. // Patched remove_waiter() implementation
  2. // The fix extracts waiter_task from waiter->task and operates on it.
  3. static void __sched remove_waiter(struct rt_mutex_base *lock,
  4. struct rt_mutex_waiter *waiter)
  5. {
  6. bool is_top_waiter = (waiter == rt_mutex_top_waiter(lock));
  7. struct task_struct *owner = rt_mutex_owner(lock);
  8. struct task_struct *waiter_task = waiter->task; // CORRECT: get task from waiter
  9. struct rt_mutex_base *next_lock;
  10. lockdep_assert_held(&lock->wait_lock);
  11. // Use scoped_guard to lock waiter_task->pi_lock, not current->pi_lock
  12. scoped_guard(raw_spinlock, &waiter_task->pi_lock) {
  13. rt_mutex_dequeue(lock, waiter);
  14. waiter_task->pi_blocked_on = NULL; // CORRECT: clear waiter's state
  15. }
  16. ...
  17. // Pass waiter_task instead of current to priority chain adjustment
  18. rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock,
  19. next_lock, NULL, waiter_task);
  20. }

5.2 縱深防禦

幾個 Kernel強化設定會影響可利用性。RANDOMIZE_KSTACK_OFFSET 對 Kernel Stack 偏移引入隨機化,使得 Stack 重用步驟變成機率性(大約 1/32 的機率)而非確定性 [1] 。kernelCTF 中的緩解目標啟用了此選項,迫使漏洞利用尋找替代路徑。

STATIC_USERMODE_HELPER 會關閉特定的 DirtyMode 路徑,阻止任意使用者模式輔助程式執行。然而,底層的寫入操作可以被重新導向到其他 sysctl 表格或可寫入的 Kernel資料結構 [1]

安全研究人員提倡多層防禦,包括 seccomp 設定檔、SELinux/AppArmor 強制執行、積極丟棄能力,以及 Kernel遙測來偵測異常的系統呼叫模式 [5] 。這些措施不能取代修補,但能降低未來本機 Kernel錯誤演變為完整 root 入侵的可能性。

6. 結論

GhostLock 說明了輔助函式中內嵌的假設,當這些函式在其原始設計者從未預期的環境中被重複使用時,會如何變成災難性的錯誤。remove_waiter() 函式對 current == waiter task 的隱含依賴在 self-blocking 路徑上維持了十五年的有效性,而 Requeue-PI 代理路徑則悄悄地違反了這個不變條件。

此漏洞攻擊鏈展示了克服現代 Kernel保護的複雜技術:利用 Prefetch 的 KASLR 繞過、用於已知位址受控記憶體的 CEA direct-map 別名、透過 PR_SET_MM_MAP 進行的 Stack 框架回收、透過 rb-tree 操作進行的受限制寫入,以及透過 DirtyMode 進行權限提升的最小 ROP。97% 的利用穩定性與 92,337 美元的 kernelCTF 獎勵,凸顯了此漏洞類別的嚴重性 [1]

未來的 Kernel開發應優先採用明確的環境傳遞,而非在清理路徑中使用隱含的 current 參照,特別是在代理操作中,當執行執行緒與受影響執行緒不同時。能夠追蹤 rtmutex 操作中任務身分的靜態分析工具,或許能在多年前就偵測到此不一致。