GhostLock(CVE-2026-43499)Linux内核futex子系统15年潜伏的栈UAF提权漏洞深度拆解

CVE-2026-43499 | CVSS 7.8 High | kernelCTF $92,337
发现团队:Nebula Security / VEGA | 披露日期:2026年7月7日


目录


1. 漏洞概述

GhostLock(CVE-2026-43499)是一个存在于Linux内核实时互斥锁(rt_mutex)优先级继承(Priority Inheritance)路径中的栈上Use-After-Free漏洞。该漏洞由Nebula Security团队的安全研究员VEGA发现,并作为IonStack研究系列的第二篇公开披露。

漏洞核心属性表:

属性
CVE编号 CVE-2026-43499
CVSS评分 7.8(High)
漏洞类型 Use-After-Free + 竞态条件
影响组件 kernel/locking/rtmutex.c - rt_mutex / futex PI路径
前置条件 CONFIG_FUTEX_PI=y(所有主流发行版默认开启)
所需权限 无需任何特殊capability或user namespace
引入时间 Linux 2.6.39-rc1(2011年5月),commit 8161239a8bcc
修复时间 Linux 7.1-rc1(2026年4月),commit 3bfdc63936dd
潜伏时长 超过15年
利用成功率 97%,约5秒获取root shell
kernelCTF奖金 $92,337
攻击向量 本地提权(LPE)+ 容器逃逸(Container Escape)

为什么这个漏洞值得关注? 不同于大多数UAF漏洞发生在堆(heap)上,GhostLock是一个罕见的栈上UAF(stack-UAF)。堆UAF的利用相对成熟,通过堆喷射(heap spray)可以较容易地重新控制释放的内存。而栈帧的生命周期极短,在函数返回后立即失效,重新回收特定栈帧并写入受控数据的技术门槛远高于堆利用。GhostLock的研究者成功攻克了这一难题,将一个看似难以利用的栈UAF转化为高达97%成功率的完整提权链。


2. 背景知识:futex与rt_mutex

2.1 futex:快速用户态互斥锁

futex(Fast Userspace muTEX)是Linux内核提供的一种高效锁机制。其设计理念是无竞争时不进入内核

  • 无竞争场景:进程在用户空间通过原子指令(如cmpxchg)直接操作锁变量,完全零内核开销
  • 有竞争场景:进程通过futex()系统调用进入内核等待队列,由内核负责调度唤醒
用户空间                        内核空间
+-------------------+           +-------------------+
| 原子操作检查锁变量 |           |                   |
| 无竞争 -> 直接获取 |           |                   |
| 有竞争 -> futex() | -------> | 等待队列管理       |
|                   | <-------  | 调度/唤醒          |
+-------------------+           +-------------------+

2.2 PI futex与优先级反转

在实时系统中,优先级反转(Priority Inversion)是一个经典问题:高优先级线程等待低优先级线程持有的锁,而低优先级线程又被中优先级线程抢占,导致高优先级线程被间接阻塞。

PI(Priority Inheritance)futex通过优先级继承协议解决这个问题:当一个高优先级线程等待低优先级线程持有的锁时,内核将高优先级"借"给低优先级线程,使其暂时以高优先级运行,尽快释放锁。

优先级反转问题                    PI解决方案
T(High)  ──等待锁──>              T(High)  ──等待锁──>
                              |
T(Low)   ──持有锁──>              T(Low)+High优先级 ──持有锁──> (快速释放)
  |                                  |
  v (被T(Med)抢占)                   v (不再被抢占)
T(Med)   ──CPU──>                   T(Med)   ──等待──>

2.3 rt_mutex与rt_mutex_waiter

内核中实现PI语义的核心数据结构是 rt_mutex(Real-Time Mutex)。当一个线程通过PI futex进入内核等待时,内核在该线程的内核栈上分配一个rt_mutex_waiter结构体:

// include/linux/rtmutex.h (简化展示)
struct rt_mutex_waiter {
    struct rt_waiter_node tree;       // 红黑树节点,嵌入在lock->waiters中
    struct rt_waiter_node pi_tree;    // PI链红黑树节点
    struct task_struct *task;         // 指向等待线程的task_struct
    struct rt_mutex_base *lock;       // 指向等待的rt_mutex
    unsigned int wake_state;          // 唤醒状态
    struct ww_acquire_ctx *ww_ctx;    // w/w acquire上下文(通常为NULL)
};

// 线程task_struct中的PI阻塞指针
struct task_struct {
    // ...
    struct rt_mutex_waiter *pi_blocked_on;  // 指向当前阻塞的waiter
    // ...
};

关键细节rt_mutex_waiter分配在等待线程的内核栈上(栈局部变量),而非堆上。这意味着一旦线程从futex系统调用返回到用户空间,该waiter所在的栈帧即被释放,pi_blocked_on如果仍指向该地址,就变成了悬垂指针(dangling pointer)

2.4 Requeue-PI机制

Requeue-PI是一种优化操作,用于高效实现条件变量(condition variable)。它允许将一个在源futex上等待的线程"迁移"到目标futex的等待队列,而无需唤醒该线程返回用户空间再重新阻塞。涉及的futex操作包括:

  • FUTEX_LOCK_PI:以PI模式获取futex锁
  • FUTEX_WAIT_REQUEUE_PI:在源futex上等待,准备被requeue到目标PI futex
  • FUTEX_CMP_REQUEUE_PI:将等待线程从源futex迁移到目标PI futex
Requeue-PI流程:

Thread-A (waiter):
  lock(f_pi_chain)
  futex(FUTEX_WAIT_REQUEUE_PI, f_wait, f_pi_target)
    --> 内核栈分配rt_mutex_waiter,阻塞在f_wait

Thread-C (requeuer):
  futex(FUTEX_CMP_REQUEUE_PI, f_wait -> f_pi_target)
    --> 尝试将waiter从f_wait迁移到f_pi_target的PI等待队列

3. 根因分析:remove_waiter()指针清理错误

3.1 Bug的精确位置

漏洞位于kernel/locking/rtmutex.c中的remove_waiter()函数。这个辅助函数最初仅为一个场景设计:线程阻塞后自己清理自己的等待状态

有缺陷的代码(修复前):

// kernel/locking/rtmutex.c
static void __sched remove_waiter(struct rt_mutex_base *lock,
                                    struct rt_mutex_waiter *waiter)
{
    // ...
    raw_spin_lock(&current->pi_lock);       // 锁定当前线程的pi_lock
    rt_mutex_dequeue(lock, waiter);         // 从锁的等待队列中移除waiter
    current->pi_blocked_on = NULL;          // ❌ BUG: 清除的是current的指针!
    raw_spin_unlock(&current->pi_lock);     // 解锁当前线程的pi_lock
    // ...
}

正确的修复代码:

// kernel/locking/rtmutex.c (修复后)
static void __sched remove_waiter(struct rt_mutex_base *lock,
                                    struct rt_mutex_waiter *waiter)
{
    // ...
    raw_spin_lock(&waiter->task->pi_lock);  // ✅ 锁定waiter所属线程的pi_lock
    rt_mutex_dequeue(lock, waiter);          // 从锁的等待队列中移除waiter
    waiter->task->pi_blocked_on = NULL;     // ✅ 正确:清除waiter所属线程的指针
    raw_spin_unlock(&waiter->task->pi_lock); // ✅ 解锁waiter所属线程的pi_lock
    // ...
}

3.2 错误的逻辑推演

问题的核心在于:remove_waiter()错误地假设current就是waiter的拥有者

在正常的慢速路径中,线程自己阻塞、自己清理,current == waiter->task,所以current->pi_blocked_onwaiter->task->pi_blocked_on指向同一个字段,代码虽然写法不够精确,但行为正确。

但在Proxy Lock路径中,通过rt_mutex_start_proxy_lock()current是发起requeue的线程(requeuer),而waiter属于另一个正在睡眠的线程。此时current != waiter->taskremove_waiter()清除的是错误的线程的pi_blocked_on

正常路径 (current == waiter->task):
  Thread-A阻塞 -> Thread-A醒来清理 -> remove_waiter()
  current->pi_blocked_on = NULL  ✅  清除的是A自己的指针

Proxy路径 (current != waiter->task):
  Thread-B阻塞(Thread-B的waiter在Thread-B的栈上)
  Thread-A调用requeue -> 内核检测死锁 -> remove_waiter()回滚
  current(=Thread-A)->pi_blocked_on = NULL  ❌  清除的是A的指针
  Thread-B的pi_blocked_on仍然指向已释放的栈内存!

3.3 触发回滚的调用路径

FUTEX_CMP_REQUEUE_PI的代理锁操作检测到PI依赖循环(死锁)时,__rt_mutex_start_proxy_lock()返回-EDEADLK,触发回滚:

int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock,
                                       struct rt_mutex_waiter *waiter,
                                       struct task_struct *task)
{
    int ret;
    raw_spin_lock_irq(&lock->wait_lock);
    ret = __rt_mutex_start_proxy_lock(lock, waiter, task);
    if (unlikely(ret))
        remove_waiter(lock, waiter);  // ret == -EDEADLK时调用,触发bug
    raw_spin_unlock_irq(&lock->wait_lock);
    return ret;
}

值得注意的是:lockdep只检查是否持有了某个pi_lock,但不会验证持有的是谁的pi_lock。因此这个错误绕过了lockdep的静态检测。


4. 漏洞触发条件

触发GhostLock需要构造一个三线程PI依赖循环,让内核的PI链遍历检测到死锁,从而进入回滚路径。

4.1 三线程拓扑

        f_pi_chain          f_pi_target          f_wait
        (PI futex)         (PI futex)         (plain futex)

Thread-W (waiter):  [持有]  ──────────────> [等待requeue到此处]
                         |                      ^
                         v                      |
Thread-O (owner):   [等待] <────────────── [持有]
                         ^
                         |
Thread-M (main):          [发起requeue: f_wait -> f_pi_target]
                         |
                         +---> 检测到循环: W->target->O->chain->W
                               返回 -EDEADLK
                               调用 remove_waiter() [BUG!]

4.2 精确的触发时序

// 伪代码:触发三线程PI循环

// Thread-W (waiter线程)
futex_lock_pi(f_pi_chain);           // 步骤1: 获取f_pi_chain
futex_wait_requeue_pi(f_wait,        // 步骤2: 在f_wait上等待
                      f_pi_target);  //         准备被requeue到f_pi_target
// 此时waiter的rt_mutex_waiter在其内核栈上

// Thread-O (owner线程)
futex_lock_pi(f_pi_target);         // 步骤3: 获取f_pi_target(requeue目标)
futex_lock_pi(f_pi_chain);           // 步骤4: 尝试获取f_pi_chain(被waiter持有)
// 此时owner被waiter阻塞,通过f_pi_chain形成依赖

// Thread-M (main/requeuer线程)
// 步骤5: 发起requeue
futex_cmp_requeue_pi(f_wait,         // 源futex
                     f_pi_target);   // 目标PI futex
// 内核尝试将waiter代理到f_pi_target
// PI链遍历: waiter -> f_pi_target -> owner -> f_pi_chain -> waiter (循环!)
// 返回 -EDEADLK,调用remove_waiter()
// BUG: 清除了main线程的pi_blocked_on,而非waiter线程的
// waiter线程带着悬垂指针返回用户空间

触发完成后,waiter线程回到用户空间,其pi_blocked_on仍然指向自己已释放的futex系统调用栈帧。之后任何引发PI链遍历的操作(如sched_setattr())都会解引用这个悬垂指针,触发Use-After-Free。


5. 利用链五阶段详解

阶段1:三线程PI循环触发UAF

如上节所述,通过三个线程和三个futex构造PI依赖循环,触发remove_waiter()的错误清理,留下悬垂指针。此阶段仅需常规的线程和futex系统调用,无需任何特殊权限。

阶段1产出: waiter.task->pi_blocked_on -> [已释放的内核栈帧]

阶段2:PR_SET_MM_MAP回收内核栈,放置伪造waiter

核心挑战:waiter对象位于内核栈上,而非堆上。栈帧在函数返回后即失效,如何重新回收特定栈帧并写入受控数据?

解决方案:利用prctl(PR_SET_MM, PR_SET_MM_MAP, ...)系统调用。该调用内部将用户态提供的auxv数据拷贝到内核栈上的一个固定大小缓冲区unsigned long user_auxv[AT_VECTOR_SIZE]。这个缓冲区恰好在waiter释放后的相近栈深度,是一大块自然对齐的受控数据区域。

waiter线程执行序列:
  1. futex_wait_requeue_pi() 返回 (栈帧释放, pi_blocked_on悬垂)
  2. 立即调用 prctl(PR_SET_MM_MAP, ...) 
     --> 内核将用户提供的auxv拷贝到栈缓冲区
     --> 缓冲区覆盖在原waiter的栈帧位置
  3. 在auxv中精心布局伪造的rt_mutex_waiter字段

伪造waiter的布局:

// 伪造的rt_mutex_waiter各字段
fake_waiter->tree      = 定制的红黑树节点(使rb_erase写入指定值)
fake_waiter->pi_tree   = 辅助节点
fake_waiter->task      = &init_task(安全可解引用的task_struct)
fake_waiter->lock      = &inet6_protos[IPPROTO_UDP] - 8(写入目标)
fake_waiter->wake_state = 0

为确保伪造waiter在消费线程读取时仍然存活,利用者使用memfd+fallocate(PUNCH_HOLE)的竞态技巧:将auxv数据跨页边界放置,同胞线程在prctl执行期间对尾部页面执行fallocate(PUNCH_HOLE),拉伸copy_from_user的时间窗口。

其他可用于回收栈帧的系统调用clonesetsockoptpselectkeyctl等,只要存在大型受控栈局部变量即可。

阶段3:红黑树删除获得受限任意写原语

当另一个线程对waiter线程调用sched_setattr()时,内核执行PI链遍历,解引用pi_blocked_on到达伪造的waiter,随后执行rt_mutex_dequeue()——一次红黑树删除操作

// PI链遍历路径:
// task->pi_blocked_on -> 伪造waiter
// fake waiter->lock -> 伪造rt_mutex_base
// rt_mutex_dequeue(lock, waiter) -> rb_erase() 写入指针

关键约束rt_mutex_dequeue()中的rb_erase操作在删除一个只有单孩子的根节点时,会将孩子指针写入根节点位置。利用者通过精心构造红黑树节点,使这个写入变成一次受限的任意指针写

写入原语: *(uint64_t *)target = W0_BASE

但存在严格约束:

  • 目标地址的前8字节必须读作"已解锁"的spinlock值
  • 写入的值W0_BASE受限于红黑树节点布局

阶段4:覆盖inet6_protos[IPPROTO_UDP]函数指针控制流劫持

利用阶段3的受限写原语,覆盖内核网络协议表中的函数指针:

// 目标: inet6_protos[IPPROTO_UDP]
// 该指针定义了IPv6 UDP协议的handler函数

// 将inet6_protos[IPPROTO_UDP]覆写为CEA(CPU Entry Area)中的伪造指针
// CEA是per-CPU的固定物理偏移区域,其direct-map地址可通过physmap基址计算
cea_direct = physmap_base + CPU1_CEA_BASE;

CEA(CPU Entry Area)的作用:CEA是x86上per-CPU的结构,保存异常、中断和系统调用入口的栈和寄存器上下文(pt_regs)。在Linux 6.2之前,CEA位于完全固定的虚拟地址;6.2之后虽然虚拟地址被随机化,但物理偏移固定,可通过prefetch侧信道泄露physmap基址后计算其direct-map别名。

利用者在CEA中存放:

  • 伪造的inet6_protocol结构体
  • pivot gadget(栈迁移指令)
  • ROP chain(返回导向编程链)

阶段5:DirtyMode修改core_pattern权限提权

最后阶段利用经典的core_pattern提权技术:

1. 发送loopback IPv6 UDP数据包
   --> 触发被覆写的inet6_protos[IPPROTO_UDP] handler
   --> 执行pivot gadget,控制流转移到CEA中的ROP chain

2. ROP chain执行一次内核写入
   --> 修改/proc/sys/kernel/core_pattern的mode权限位
   --> 将core_pattern设置为攻击者指定的程序路径

3. 在用户空间触发crash(如raise(SIGSEGV))
   --> 内核执行core_pattern指定的程序,以root权限运行
   --> 获取root shell

DirtyMode的含义:一旦core_pattern的权限被修改,后续的提权完全在用户空间完成,不需要进一步的内核利用。


6. 完整攻击链架构图

+===================================================================+
|                    GhostLock 完整攻击链架构图                       |
+===================================================================+

  用户空间 (unprivileged process)
  +-------------------------------------------------------------+
  |                                                               |
  |  [阶段0] ASLR信息泄露                                        |
  |    prefetch侧信道 --> KASLR基址 + physmap基址                 |
  |          |                                                    |
  |          v                                                    |
  |  [阶段1] 构造三线程PI循环                                      |
  |    Thread-W: lock(f_pi_chain) + FUTEX_WAIT_REQUEUE_PI        |
  |    Thread-O: lock(f_pi_target) + lock(f_pi_chain)            |
  |    Thread-M: FUTEX_CMP_REQUEUE_PI                              |
  |          |                                                    |
  |          v                                                    |
  |    内核检测死锁 -> remove_waiter() [BUG]                       |
  |    Thread-W.pi_blocked_on -> [悬垂指针]                        |
  |          |                                                    |
  |          v                                                    |
  |  [阶段2] 栈回收 + 伪造waiter                                  |
  |    Thread-W: prctl(PR_SET_MM_MAP, ...)                        |
  |    +-----------+--------+--------+--------+--------+         |
  |    | tree      | pi_tree| task   | lock   |wake_st |         |
  |    | (rb节点)  |        |&init   |target-8| 0      |         |
  |    +-----------+--------+--------+--------+--------+         |
  |          |                                                    |
  |          v                                                    |
  |  [阶段3] 受限任意写原语                                        |
  |    sched_setattr(Thread-W) -> PI链遍历 -> 解引用伪造waiter    |
  |    rt_mutex_dequeue() -> rb_erase -> *(target) = W0_BASE     |
  |          |                                                    |
  |          v                                                    |
  |  [阶段4] 函数指针覆写 + CFH                                    |
  |    inet6_protos[IPPROTO_UDP] = CEA_direct (伪造handler)       |
  |    发送IPv6 UDP(loopback) -> 调用伪造handler -> ROP pivot      |
  |          |                                                    |
  |          v                                                    |
  |  [阶段5] DirtyMode提权                                        |
  |    ROP: 修改core_pattern mode位                                |
  |    设置core_pattern -> "/tmp/pwn"                              |
  |    用户空间crash -> root执行pwn -> root shell                 |
  |                                                               |
  +-------------------------------------------------------------+
           ||                        ||                   ||
  =========||========================||===================||=====
           ||                        ||                   ||
  +--------vv--------+  +-----------vv-----------+  +----vv----+
  | 内核栈 UAF      |  | inet6_protos[]覆写      |  |core_pat |
  | (futex wait帧)  |  | (网络协议handler表)     |  |tern覆写 |
  +------------------+  +-------------------------+  +---------+
           ||                        ||                   ||
  +--------vv--------+  +-----------vv-----------+  +----vv----+
  | rt_mutex_waiter  |  | CPU Entry Area (CEA)   |  |/proc/   |
  | 悬垂指针         |  | 存放: 伪造protocol     |  |sys/kernel|
  | pi_blocked_on    |  |       pivot gadget     |  |/core_pat |
  | -> freed stack   |  |       ROP chain        |  |tern      |
  +------------------+  +-------------------------+  +---------+

+===================================================================+
|  数据流: ASLR泄露 -> UAF触发 -> 栈回收 -> 受限写 -> CFH -> LPE   |
+===================================================================+

7. 影响范围与版本矩阵

7.1 受影响版本

发行版 受影响内核范围 状态
主线内核 2.6.39-rc1 ~ 7.1-rc1 已在7.1-rc1修复
Debian 13 已确认受影响 已修复
Debian 12 已确认受影响 修复待定
Debian 11 已确认受影响 尚无errata
Ubuntu 24.04 已确认受影响 已修复
RHEL/CentOS 7 已确认受影响 修复待定
RHEL/CentOS 8 已确认受影响 修复待定
RHEL 9 / CentOS Stream 9 kernel-5.14.0-699.el9及更早 已在5.14.0-716.el9修复
RHEL 10 / CentOS Stream 10 kernel-6.12.0-224.el10及更早 已在6.12.0-239.el10修复
AlmaLinux 9/10 跟踪EL9/EL10内核 已修复
Proxmox VE 8 已确认受影响 已修复
Talos Linux 已确认受影响 v1.13.6已修复(内核6.18.38-talos)

7.2 修复回溯到的稳定版本线

上游修复commit 3bfdc63936dd已回溯到以下LTS分支:

  • 6.1.175
  • 6.6.140
  • 6.12.86
  • 6.18.27
  • 7.0.4

7.3 容器逃逸风险

GhostLock的攻击面不仅限于本地提权,还可实现容器逃逸。容器内的非特权进程通过futex PI操作到达宿主机的内核代码路径,除非容器runtime已经通过seccomp过滤了相关futex操作,否则容器内攻击者可以获取宿主机的root权限。

容器安全建议:在容器seccomp profile中阻断以下futex操作可有效关闭触发路径:

  • FUTEX_LOCK_PI
  • FUTEX_WAIT_REQUEUE_PI
  • FUTEX_CMP_REQUEUE_PI
  • FUTEX_LOCK_PI2(较新内核)
  • futex_time64系统调用变体

8. 检测方案

8.1 内核版本检测脚本

#!/bin/bash
# ghostlock-check.sh - GhostLock (CVE-2026-43499) 快速检测脚本
# 用法: sudo bash ghostlock-check.sh

echo "=========================================="
echo " GhostLock (CVE-2026-43499) 检测脚本"
echo "=========================================="

# 获取内核版本
KVER=$(uname -r)
echo "[*] 当前内核版本: $KVER"

# 提取主版本号
KMAJOR=$(echo "$KVER" | cut -d. -f1)
KMINOR=$(echo "$KVER" | cut -d. -f2 | cut -d- -f1)
KPATCH=$(echo "$KVER" | cut -d. -f3 | cut -d- -f1)

# 检查是否在受影响范围内 (2.6.39 - 7.0.x)
AFFECTED=0

if [ "$KMAJOR" -eq 2 ] && [ "$KMINOR" -eq 6 ]; then
    if [ "$KPATCH" -ge 39 ]; then
        AFFECTED=1
    fi
elif [ "$KMAJOR" -ge 3 ] && [ "$KMAJOR" -le 6 ]; then
    AFFECTED=1
elif [ "$KMAJOR" -eq 7 ] && [ "$KMINOR" -eq 0 ]; then
    AFFECTED=1
fi

# 检查CONFIG_FUTEX_PI是否开启
if [ -f /boot/config-"$KVER" ]; then
    FUTEX_PI=$(grep "CONFIG_FUTEX_PI" /boot/config-"$KVER" | cut -d= -f2)
elif [ -f /proc/config.gz ]; then
    FUTEX_PI=$(zcat /proc/config.gz | grep "CONFIG_FUTEX_PI" | cut -d= -f2)
else
    FUTEX_PI="unknown (config not found)"
fi

echo "[*] CONFIG_FUTEX_PI: $FUTEX_PI"

# 检查内核符号表中的修复标记
if [ -f "/proc/kallsyms" ] && [ -r "/proc/kallsyms" ]; then
    # 检查remove_waiter函数是否包含修复后的waiter->task引用
    REMOVE_WAITER=$(sudo cat /proc/kallsyms 2>/dev/null | grep -c "remove_waiter" || echo "0")
    echo "[*] remove_waiter符号: $REMOVE_WAITER 个匹配"
fi

# 检查seccomp是否过滤了futex PI操作
echo "[*] seccomp状态检查:"
if command -v seccomp-status &>/dev/null; then
    seccomp-status 2>/dev/null || echo "    无法获取seccomp状态"
else
    echo "    (需要安装seccomp工具进行详细检查)"
fi

# 结果判定
echo ""
echo "=========================================="
if [ "$AFFECTED" -eq 1 ]; then
    if [ "$FUTEX_PI" = "y" ]; then
        echo " [!!] 警告: 该内核可能受 GhostLock 影响"
        echo " [!!] 建议立即升级到修复版本"
        echo " [!!] 临时缓解: 部署seccomp过滤futex PI操作"
    else
        echo " [*] CONFIG_FUTEX_PI未开启,不受影响"
    fi
else
    echo " [OK] 内核版本不在受影响范围内"
fi
echo "=========================================="
echo ""
echo "参考修复版本:"
echo "  6.1.175 / 6.6.140 / 6.12.86 / 6.18.27 / 7.0.4+"
echo "  修复commit: 3bfdc63936dd"

8.2 运行时行为检测

通过监控异常的futex PI操作序列可进行运行时检测:

# 使用auditd监控可疑的futex PI操作模式
# 三线程PI循环的触发需要频繁的FUTEX_LOCK_PI / FUTEX_WAIT_REQUEUE_PI / 
# FUTEX_CMP_REQUEUE_PI操作组合

sudo auditctl -a always,exit -F arch=b64 -S futex -F a0=0xc \
    -F a1=0x1 -k ghostlock_detect
sudo auditctl -a always,exit -F arch=b64 -S futex -F a0=0xd \
    -k ghostlock_detect
sudo auditctl -a always,exit -F arch=b64 -S futex -F a0=0xe \
    -k ghostlock_detect

9. 缓解措施

9.1 已知缓解方案对比

缓解方案 效果 代价 推荐度
升级内核(应用commit 3bfdc63936dd 完全修复 需要重启 强烈推荐
KernelCare / livepatch 完全修复,免重启 需要订阅 强烈推荐
seccomp过滤futex PI操作 阻断触发路径 破坏使用PI锁的工作负载 推荐用于容器
kernel.randomize_kstack_offset=1 降低利用可靠性 非完全缓解 辅助措施
KPTI启用 增加prefetch泄露难度 已在大多数发行版默认启用 辅助措施
STATIC_USERMODE_HELPER 阻断core_pattern提权路径 可能影响合法crash handler 辅助措施

9.2 无通用运行时缓解方案

需要特别强调的是:不存在干净、通用、主机范围的运行时缓解方案CONFIG_FUTEX_PI是编译时选项,没有运行时开关。它是glibc PTHREAD_PRIO_INHERIT互斥锁所必需的,无法在不破坏用户空间程序的情况下禁用。没有模块可以blacklist,也没有设置可以翻转。

9.3 容器环境缓解

对于容器化环境,seccomp策略是唯一有效的运行时阻断手段:

// containerd / Docker seccomp profile 补充规则
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["futex", "futex_time64"],
      "args": [
        {
          "index": 0,
          "op": "SCMP_CMP_MASKED_EQ",
          "mask": 0x0f,
          "value": 0x0c
        }
      ],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 22
    }
  ]
}

注意:上述规则会阻断所有使用PI语义的futex操作。在部署前务必测试工作负载兼容性。


10. 修复补丁分析

10.1 主修复补丁

修复commit 3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")是一个精确的一行修正

--- a/kernel/locking/rtmutex.c
+++ b/kernel/locking/rtmutex.c
@@ static void __sched remove_waiter(struct rt_mutex_base *lock,
                                      struct rt_mutex_waiter *waiter)
 {
     // ...
-    raw_spin_lock(&current->pi_lock);
+    raw_spin_lock(&waiter->task->pi_lock);
     rt_mutex_dequeue(lock, waiter);
-    current->pi_blocked_on = NULL;
+    waiter->task->pi_blocked_on = NULL;
-    raw_spin_unlock(&current->pi_lock);
+    raw_spin_unlock(&waiter->task->pi_lock);
     // ...
 }

10.2 修复的本质

修复的核心是让remove_waiter()操作正确的线程——不是current(执行清理的线程),而是waiter->task(实际拥有waiter的线程)。这样无论remove_waiter()是被直接调用还是通过proxy路径调用,都能正确清理对应的pi_blocked_on指针。

修复前:
  remove_waiter()回滚时:
    current->pi_blocked_on = NULL    (requeuer的指针被清除)
    waiter->task->pi_blocked_on 仍指向已释放栈帧  <-- UAF

修复后:
  remove_waiter()回滚时:
    waiter->task->pi_blocked_on = NULL  (waiter的指针被正确清除)
    没有悬垂指针残留  <-- 安全

10.3 关于CVE-2026-53166

后续有一个跟进修改commit 74e144274af3,曾被单独追踪为CVE-2026-53166,但后来因引入回归(regression)被上游revert。该CVE不应被视为GhostLock的必要修复条件。确认GhostLock修复状态时,仅需验证commit 3bfdc63936dd是否已合入。

10.4 补丁兼容性

由于这是一个小而自包含的锁路径修改,不涉及任何数据结构布局变更,因此是一个干净的rebootless patch候选,可跨内核系列应用livepatch。


11. 深层启示

11.1 "假设current就是owner"的隐性风险

GhostLock揭示了一类经典的内核编程反模式:辅助函数隐式假设调用者身份remove_waiter()在原始设计中只服务于一种调用场景,但后来被Requeue-PI机制复用到了一个完全不同的调用路径。原始的隐式假设(current == waiter->task)在新路径中不再成立,但代码中没有显式的断言或文档说明这个假设前提。

这类问题在内核代码库中可能并不罕见。任何辅助函数如果依赖于current与某个参数之间的隐式等价关系,在函数被复用到新路径时都可能产生类似的安全问题。

11.2 栈UAF的利用时代

GhostLock标志着栈UAF利用技术的成熟。传统上,安全研究者和防御者认为栈上漏洞难以利用(因为栈帧生命周期短、回收不可控)。但通过精确的栈帧深度匹配(找到恰好在相同栈深度放置大型受控数据的系统调用)和竞态技巧(如memfd+fallocate(PUNCH_HOLE)拉伸拷贝窗口),研究者证明了栈UAF可以被可靠利用。

这对内核防御提出了新的要求:仅依赖ASLR和堆防护已不足够,栈层面的防护(如RANDOMIZE_Kstack_OFFSET)也需要得到重视。

11.3 15年潜伏的反思

这个漏洞从2011年引入到2026年被发现,在Linux内核中存在了超过15年。期间经历了无数代码审查、 fuzzing campaign和安全审计,却始终未被捕获。原因可能包括:

  • 触发条件复杂:需要三线程PI循环,常规fuzzing难以覆盖
  • Proxy路径冷门:Requeue-PI是相对少用的操作
  • lockdep盲区:lockdep不验证锁的持有者身份
  • 栈UAF非常规:大多数审计关注堆层面的use-after-free

这提醒安全社区,对于长期存在的内核基础设施代码,需要特别关注那些"几乎正确但假设了隐式前提"的路径。

11.4 容器逃逸的深层含义

GhostLock的容器逃逸能力再次凸显了一个关键事实:容器隔离依赖于内核的正确性。容器本质上共享宿主机内核,任何内核本地提权漏洞自动成为容器逃逸漏洞。在选择隔离方案时,对于高安全需求的场景,基于虚拟化(VM)的隔离(如Cozystack/Talos采用的guest VM方案)天然免疫此类攻击。


12. 参考资料

资源 链接
Nebula Security 研究文章 https://nebusec.ai/research/ionstack-part-2/
修复commit 3bfdc63936dd https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3bfdc63936dd
引入commit 8161239a8bcc https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=8161239a8bcc
NVD条目 https://nvd.nist.gov/vuln/detail/CVE-2026-43499
PoC代码 https://github.com/NebuSec/CyberMeowfia
TuxCare技术分析 https://tuxcare.com/blog/ghostlock-cve/
AlmaLinux安全公告 https://almalinux.org/cs/blog/2026-07-09-ghostlock/
CloudLinux分析 https://blog.cloudlinux.com/ghostlock-cve-2026-43499-local-root-exploit-kernel-update-for-cloudlinux/
prefetch侧信道论文 https://gruss.cc/files/prefetch.pdf
EntryBleed (KPTI下ASLR泄露) https://www.willsroot.io/2022/12/entrybleed.html
Project Zero CEA利用 https://googleprojectzero.blogspot.com/2022/12/exploiting-CVE-2022-42703-bringing-back-the-stack-attack.html

声明:本文仅用于安全技术研究和教育目的。未经授权利用内核漏洞属于违法行为。请及时升级系统内核以消除安全风险。