1. 簡介

Linux 核心的本機權限提升(Local privilege escalation ,LPE)研究日益將手動子系統分析與 AI 輔助的漏洞發現相結合。一個具代表性的案例是在流量控制排程器子系統 net/sched 中發現的釋放後使用(Use-After-Free, UAF)漏洞,後來被指派為 CVE-2026-53264 [1] 。該缺陷源於用於查詢共享核心 action 物件的鎖定規則與用於釋放該物件的規則之間的不匹配,產生了一個狹窄但可利用的競爭視窗(Race window)。此報告分析了根本原因、用於將競爭視窗從大約十五分鐘壓縮到幾秒鐘的工程技術,以及將 UAF 轉換為核心任意寫入的回收/ pivot 鏈。

AI 5 秒破解 Linux 核心防線?RCU同步一個疏漏,Qdisc 竟成核心奪權跳板? | 資訊安全新聞

2. 子系統背景:Action 引用計數

net/sched 管控傳出封包在傳輸前如何被分類和處理。裝置的佇列規則(Queueing discipline, Qdisc)會參照篩選器鏈,而篩選器則會呼叫 action 。為了讓 action 能在相同網路命名空間內跨篩選器重複使用,每個 action 都會在每個命名空間的 radix tree action_idr 中註冊,並以整數識別碼作為索引。依索引查詢和(條件式)配置 action 是由 tcf_idr_check_alloc() 處理,如下所示 [1]

  1. int tcf_idr_check_alloc(struct tc_action_net *tn, u32 *index,
  2. struct tc_action **a, int bind)
  3. {
  4. struct tcf_idrinfo *idrinfo = tn->idrinfo;
  5. struct tc_action *p;
  6. int ret;
  7. u32 max;
  8. if (*index) {
  9. rcu_read_lock();
  10. p = idr_find(&idrinfo->action_idr, *index); // [0]: ACTION LOOKUP
  11. // [1]: WINDOW OPENS
  12. if (IS_ERR(p)) {
  13. rcu_read_unlock();
  14. return -EAGAIN;
  15. }
  16. if (!p) {
  17. max = *index;
  18. rcu_read_unlock();
  19. goto new;
  20. }
  21. // [2]: WINDOW CLOSES
  22. if (!refcount_inc_not_zero(&p->tcfa_refcnt)) {
  23. rcu_read_unlock();
  24. return -EAGAIN;
  25. }
  26. if (bind)
  27. atomic_inc(&p->tcfa_bindcnt);
  28. *a = p;
  29. rcu_read_unlock();
  30. return 1;
  31. } else {
  32. *index = 1;
  33. max = UINT_MAX;
  34. }
  35. new:
  36. *a = NULL;
  37. mutex_lock(&idrinfo->lock);
  38. ret = idr_alloc_u32(&idrinfo->action_idr, ERR_PTR(-EBUSY), index, max,
  39. GFP_KERNEL);
  40. mutex_unlock(&idrinfo->lock);
  41. if (ret == -ENOSPC && *index == max)
  42. ret = -EAGAIN;
  43. return ret;
  44. }

程式碼 1 — 帶有競爭視窗邊界註解 [0]–[2] 的 Action 查詢常式 [1]

3. 根本原因:鎖定衝突的競爭視窗

程式碼 1 中的查詢路徑僅持有 rcu_read_lock() ,這是一個 RCU 讀取端 critical section,它能保證指標仍可解參照(Dereference),但 無法 防止物件被釋放和重複使用。相比之下,action 刪除是在 idrinfo->lock rtnl_lock() 下執行,並使用 raw kfree() — 沒有使用 synchronize_rcu() call_rcu() 延遲 [1] 。因此,在 idr_find() 的指標讀取和 refcount_inc_not_zero() 的引用計數增加之間,第二個執行緒可以釋放該物件,而第三個執行緒可以用攻擊者控制的內容來回收已釋放的記憶體,包括在 tcfa_refcnt 偏移處設置一個非零值。這是一個教科書級的配置器競爭,但其可利用性完全取決於能否命中一個狹窄的三步 interleaving:

sequenceDiagram participant C0 as CPU 0 (lookup) participant C1 as CPU 1 (delete) participant C2 as CPU 2 (reclaim) C0->>C0: idr_find(action_idr, index) -> p Note over C0: window opens [1] C1->>C1: kfree(p) C2->>C2: allocate same slab slot C2->>C2: attacker sets p->tcfa_refcnt != 0 Note over C0: window closes [2] C0->>C0: refcount_inc_not_zero(&p->tcfa_refcnt) Note over C0: succeeds on stale/reclaimed object -> UAF

圖 1 — 查詢、刪除和回收之間的三方競爭,衍生自主要來源的競爭圖 [1]

4. 透過 Netlink 到達漏洞路徑

直接呼叫 RTM_NEWACTION / RTM_DELACTION 需要在初始命名空間中具備 CAP_NET_ADMIN ,這對非特權程序來說是不可用的。該查詢常式可間接透過 RTM_NEWTFILTER / RTM_DELTFILTER 到達,因為建立或刪除篩選器也會建立或釋放對其列出的 action 的參照。關鍵在於, tc_new_tfilter() 僅在某些條件下才會獲取全域 rtnl_lock() [1]

  1. if (rtnl_held ||
  2. (q && !(q->ops->cl_ops->flags & QDISC_CLASS_OPS_DOIT_UNLOCKED)) ||
  3. !tcf_proto_is_unlocked(name)) {
  4. rtnl_held = true;
  5. rtnl_lock();
  6. }

程式碼 2 — 篩選器建立路徑中的條件式鎖定獲取 [1]

透過選擇 clsact Qdisc 搭配 flower 分類器 — 兩者都設定了 DOIT_UNLOCKED 旗標 — 攻擊者可以避免在單一全域 rtnl_lock() 上序列化,允許多個競爭執行緒同時執行漏洞路徑。從非特權程序到達此程式碼還需要一個非特權使用者命名空間,以滿足在 rtnetlink_rcv_msg() 中執行的命名空間層級 CAP_NET_ADMIN 檢查 [1]

5. 壓縮競爭視窗

要可靠地命中圖 1 中的三步交錯,需要分層最佳化。首先,一個由 timerfd 和長 epoll 等待者清單構建的通用視窗擴大操作(Window-widening primitive),能將競爭中的 CPU 停滯一個可調整的、微秒級別的間隔,允許 spin-loop 掃描將 netlink 呼叫的時間與刪除事件對齊。其次,將競爭執行緒分散到獨立的排程器 上,避免了在 tcf_chain_tp_find() 中獲取的共享 per-chain mutex 上的爭用。第三,也是最重要的一點,該競爭圍繞兩種執行緒角色進行了重構,這些角色重用了 action 初始化的錯誤路徑,而不是每次執行都付出完整篩選器建立/刪除週期的成本 [1]

  1. for (i = 1; i <= TCA_ACT_MAX_PRIO && tb[i]; i++) {
  2. act = tcf_action_init_1(net, tp, tb[i], est, ops[i - 1],
  3. &init_res[i - 1], flags, extack); // [0]
  4. if (IS_ERR(act)) {
  5. err = PTR_ERR(act);
  6. goto err; // [1]
  7. }
  8. actions[i - 1] = act;
  9. }
  10. tcf_idr_insert_many(actions, init_res); // [2]
  11. err:
  12. tcf_action_destroy(actions, flags & TCA_ACT_FLAGS_BIND);

程式碼 3 — 被 binder 執行緒利用的 tcf_action_init() 錯誤路徑 [1]

Binder 一個 binder 執行緒提交了一個在索引 42 處帶有有效動作的過濾器,接著再提交一個刻意構造錯誤的第二個動作,迫使程式碼 3 中的迴圈在標籤處 [1] 中止,在 tcf_idr_insert_many() 執行之前;這會在每次執行時觸發查詢 ([0]),而無需插入一個後續需要拆除的篩選器。單一 刪除者 執行緒在相同索引上持續建立和刪除一個單一 action 的篩選器,這會重新填充和釋放 idr 槽位。在獨立的 CPU 上執行 N 個 binder 執行緒對抗一個刪除者執行緒,將觸發競爭的時間從超過十五分鐘減少到大約五秒 [1]

sequenceDiagram participant B as Binder threads (N) participant D as Deleter thread (1) participant IDR as action_idr[42] loop repeatedly D->>IDR: create filter (insert action 42) D->>IDR: delete filter (free action 42) end loop repeatedly B->>IDR: create filter, action[0]=42 (valid) B->>IDR: action[1] malformed -> abort at err path Note over B: idr_find(42) executed every attempt, no cleanup needed end

圖 2 — 最終的 binder/刪除者競爭配置 [1]

6. 從 UAF 到控制流劫持

一旦競爭成功,被釋放的 tc_action 物件(從 kmalloc-256 快取配置)會透過重複呼叫 KEYCTL_UPDATE 取得的 user_key_payload 物件來回收。因為 user_key_payload rcu.next 欄位與 tc_action_ops *ops vtable 指標的偏移重疊,攻擊者獲得了完全受控制的函式指標覆寫。 user_update() 使用的 RCU 延遲 — call_rcu(&zap->rcu, user_free_payload_rcu) — 既提供了回收向量,也提供了一個方便的連結串列結構用於 spraying,因為每個被釋放的 payload 都會透過 rcu.next 鏈結,直到寬限期結束 [1] 。觸發被劫持的 vtable 進入點 tcf_action_fill_size() 被選為目標,因為在 CentOS 9 目標上,其前綴會使暫存器 rbp 指向已回收的物件,從而能夠透過一個四個 pop 的 gadget 鏈,將 stack pivot 到攻擊者控制的資料上 [1]

7. 從 RIP 控制到任意寫入

與其依賴 Sprayed BPF JIT gadget(作者發現這在不同機器配置上不可靠),pivot 後的 stack 執行一個簡短的 return-oriented chain,該 chain 會覆寫 core_pattern sysctl,使其指向一個指向漏洞利用二進位檔案本身的 pipe handler,這是一種透過核心傾印處理程式(Core-dump handler)獲得 root 執行的知名技術 [1]

  1. ((u64 *)keyctl_payload)[0] = NULL_AT;
  2. ((u64 *)keyctl_payload)[1] = POP_RDI_RET;
  3. ((u64 *)keyctl_payload)[2] = NULL_AT;
  4. ((u64 *)keyctl_payload)[3] = POP_RSI_RET;
  5. ((u64 *)keyctl_payload)[4] = (u64)&pivot_rop;
  6. ((u64 *)keyctl_payload)[5] = POP_RDX_RET;
  7. ((u64 *)keyctl_payload)[6] = sizeof(pivot_rop);
  8. ((u64 *)keyctl_payload)[7] = COPY_FROM_USER;
  9. ((u64 *)keyctl_payload)[8] = POP_RSP_RET;
  10. ((u64 *)keyctl_payload)[9] = NULL_AT;
  11. ((u64 *)keyctl_payload)[11] = STACK_PIVOT; // called as p->ops->get_fill_size

程式碼 4 — 初始 ROP 階段,pivot 到第二階段緩衝區 [1]

後續的除錯過程顯示,在刻意插入的 msleep() 期間發生環境切換時,將核心 stack pointer 留在已回收的 heap 物件內會損壞相鄰的配置;修復方式是將最終的 pivot 移到一個未使用的、零填充的 .ktext 區域,這樣暫時的核心 stack 就不會覆寫有效的 heap 資料 [1] 。這個細節說明了核心漏洞利用中一個反覆出現的主題:主要操作的正確性是必要的,但還不夠 — 對次要副作用(例如 sleep 期間的 stack 位置)的控制,決定了漏洞利用在重複執行下的可靠性。

8. 核心記憶體損壞的比較視角

將這種依賴機率和時間的競爭,與確定性的核心記憶體損壞操作進行對比是有啟發性的。在一個不相關但結構上可比較的案例中,AF_ALG 子系統中的 zero-copy 解密路徑允許頁面快取的頁面直接被可寫入的 scatterlist 參照,產生了一種完全沒有任何 race condition 的記憶體內損壞操作 [2] 。AF_ALG 案例透過利用 zero-copy 資料路徑中的設計假設來實現可攜性和確定性,而 net/sched 案例則必須針對真正的多核心競爭來設計統計可靠性,這說明了將邏輯缺陷轉化為可運作的核心操作的兩種不同工程理念 — 一種是確定性和結構性的,另一種是機率和依賴時序的。

9. AI 輔助發現的脈絡

潛在的錯誤是在 AI 輔助下定位的,而非透過詳盡的手動子系統研究,而且在該漏洞利用公開亮相的幾天前,一個自動化系統也獨立報告了相同程式碼區域中的一個類似錯誤 [1] 。這反映了更廣泛的趨勢,即 AI 驅動的代理已開始在成熟、經過大量模糊測試的程式庫中浮現記憶體安全缺陷,這些缺陷先前既逃過了人工審查,也逃過了傳統的模糊測試,例如一個 AI 研究代理在 SQLite 公開釋出之前發現的堆疊緩衝區下溢 [3] 。這兩個案例都表明,AI 工具在發現階段越來越有效,而精確的武器化 — 競爭視窗工程、回收物件選擇和 gadget 放置 — 仍然取決於研究人員自身的子系統層級推理能力。

10. 結論

這個案例研究展示了一個完整的 LPE 鏈,它建立在一個單一的鎖定規則衝突之上:一個僅能透過非特權 netlink 篩選器操作到達的 UAF,透過利用計時器的停滯和錯誤路徑重複使用技巧壓縮到五秒鐘的競爭,並透過使用 RCU 的 heap 梳理和依賴於暫存器狀態的 stack pivot,轉換為核心任意寫入。其結果 CVE-2026-53264 強調,即使是經過良好審查的引用計數子系統,當 RCU 讀取端保證被默默地與非 RCU 延遲釋放配對時,仍然存在漏洞,而且競爭視窗工程仍然是將理論錯誤與可靠漏洞利用區分開來的決定性因素。