0/6 已展开

LLM 分析

futex requeue-PI:rt_mutex 调度注解错位与 rcuwait UAF 修复

系列概况

  • 标题: [PATCH v4 0/2] futex: Address two futex-requeue-pi issues
  • 作者: Sebastian Andrzej Siewior bigeasy@linutronix.de(patch 2 共作者 Yao Kai)
  • 版本: v4(前序 v1/v2/v3 与 Yao Kai 原始系列)
  • 规模: 5 个文件,+33 / -15 行
  • 修改文件: include/linux/sched/rt.hkernel/futex/pi.ckernel/futex/requeue.ckernel/locking/rtmutex_api.ckernel/sched/core.c
  • 代码统计: patch 1 = 4 files +23/-13;patch 2 = 1 file +10/-2
  • Message-ID: 20260901135453.3121948-1-bigeasy@linutronix.de(首封)
  • 完整性: 完整系列:0/2 封面 + 1/2 + 2/2 patch,作者追加一封复现建议回复,两封 tip-bot 公告。

补丁目的

修复 FUTEX_CMP_REQUEUE_PI 路径上两个长期被忽视的问题:

  1. futex proxy 锁路径上的 rt_mutex 调度注解错位futex_wait_requeue_pi()rt_mutex_wait_proxy_lock() 漏掉了 sched_rt_mutex 注解;原 rt_mutex_pre_schedule() / rt_mutex_post_schedule() 被错误地贴在 futex_lock_pi() 里,且在彼处调用 sched_submit_work() 是不合理的(PeterZ 指出)。
  2. futex_requeue_pi_complete() 越权唤醒栈对象:waiter 在 Q_REQUEUE_PI_LOCKED 状态下提前退出 syscall,futex_q(栈对象)已经销毁,requeue 端仍调 rcuwait_wake_up(&q->requeue_wait) 触发 KASAN slab-out-of-bounds。

旧流程的问题

  • futex_lock_pi() 在 hb->lock 下调用 rt_mutex_pre_schedule() / rt_mutex_post_schedule(),但彼时 pi_waiter 尚未入队,sched_submit_work() 本就无事可做,反而把 sched_rt_mutex 断言的位置搅浑。
  • futex_requeue_pi_complete() 看到 Q_REQUEUE_PI_WAIT 时无条件 rcuwait_wake_up(),未区分 waiter 仍睡在 rcuwait 还是已经离开,把"已走人"的栈对象再次叫醒。

新流程

  • 新增 rt_mutex_futex_pre_schedule() / rt_mutex_futex_post_schedule():只翻转 current->sched_rt_mutex,并 lockdep 断言 PF_WQ_WORKER / PF_IO_WORKER / plug 都为假。
  • 把这两个注解移到 rt_mutex_wait_proxy_lock() 内部、wait_lock 释放前后,覆盖所有走 proxy 锁的 futex 路径;同时从 futex_lock_pi() 移除旧注解。
  • futex_requeue_pi_complete() 仅在 old == Q_REQUEUE_PI_WAIT && new != Q_REQUEUE_PI_LOCKEDrcuwait_wake_up(),其余情况改由 requeue_pi_wake_futex() 通过 wake_up_state() 兜底。

Patch 概览

  • patch 1(Sebastian):新增 futex 专用 rt_mutex_futex_pre/post_schedule(),从 futex_lock_pi() 删除旧注解,统一在 rt_mutex_wait_proxy_lock() 内部加注解。
  • patch 2(Yao Kai,Sebastian 更新注释):在 Q_REQUEUE_PI_WAIT → Q_REQUEUE_PI_LOCKED 转换时跳过 rcuwait_wake_up(),避免对已释放的栈对象做唤醒。

关键实现

patch 1:futex 专用 rt_mutex 调度注解

void rt_mutex_futex_pre_schedule(void)
{
    lockdep_assert(!(current->flags & (PF_WQ_WORKER | PF_IO_WORKER)));
    lockdep_assert(!current->plug);
    lockdep_assert(!fetch_and_set(current->sched_rt_mutex, 1));
}

void rt_mutex_futex_post_schedule(void)
{
    lockdep_assert(fetch_and_set(current->sched_rt_mutex, 0));
}

rt_mutex_wait_proxy_lock() 内紧贴 schedule() 处包住,确保所有走 proxy 锁的 futex 路径满足 sched_rt_mutex 断言契约,但调用 sched_submit_work()(syscall 上下文没有需要 flush 的 I/O,也不是需要通知的 worker)。

patch 2:跳过对已离开 waiter 的 rcuwait 唤醒

if (unlikely(old == Q_REQUEUE_PI_WAIT) && new != Q_REQUEUE_PI_LOCKED)
    rcuwait_wake_up(&q->requeue_wait);

当状态被推到 Q_REQUEUE_PI_LOCKED,说明 waiter 在 rcuwait_wait_event() 读到该值并提前 break 退栈;唤醒责任交由 requeue_pi_wake_futex() 通过 wake_up_state(q->task, ...) 兜底,且 hb->lock 仍持有、q->task 受 RCU 保护。

waiter                               requeue task
------                               -------------
futex_wait_requeue_pi()
  futex_do_wait() -> schedule()      futex_requeue
                                      futex_proxy_trylock_atomic()
                                      futex_requeue_pi_prepare()
                                        NONE -> IN_PROGRESS
(timeout/signal wakes waiter)
futex_requeue_pi_wakeup_sync()
  IN_PROGRESS -> WAIT               requeue_pi_wake_futex
                                      saves q->task
                                      futex_requeue_pi_complete()
                                        WAIT -> LOCKED
                                        [skip rcuwait_wake_up here]
  rcuwait sees LOCKED, breaks         wake_up_state(saved_task)
  returns from syscall -> q freed

类比

futex_q 想象成 waiter 写在餐巾纸上的排队号。服务员(requeue 任务)撕下一张"已叫号"券(Q_REQUEUE_PI_LOCKED)准备喊号时,客人已经吃完饭把餐巾纸扔进垃圾桶了。修复就是:服务员发现客人已经离开(rcuwait 没在 sleep)时不要对着垃圾桶喊,而是改走正门叫号的广播(wake_up_state())。Patch 1 则相当于把"准备离开"的提示贴到了真正离开的前一步(proxy 锁 sleep 之前),而不是错误地贴在前台叫号机旁。

Highlight:风险与注意点

  • 第一轮跳过 optimistic spin 的调试技巧(message 4):通过 first_loop = 1 强制 rtmutex_slowlock_block 第一次循环直接进入 sleep,让"通常不竞争"的路径更可靠触发 sched_rt_mutex 断言,但该片段未进入合并版本。
  • 唤醒兜底依赖 wake_up_state() + hb->lock + RCU 假设:patch 2 的安全性完全建立在 q->task 在 hb->lock 持锁期间通过 RCU 访问仍然有效;若未来有人移除 RCU 保护或改变锁顺序,需要重新评估 UAF。
  • 删除 futex_lock_pi() 中的注解不等于放任 work flush:proxy 锁路径仍依赖 rtmutex 内部对 sched_submit_work() 的处理;PREEMPT_RT 上 mutex 与 futex proxy 锁共用同一抽象,需要重新确认 futex_lock_pi() 调用栈不会出现"既要 flush I/O 又不调用 sched_submit_work"的退化路径。
  • v3→v4 关键改动:根据 PeterZ 意见,放弃在已入队 pi_waiter 上调用 sched_submit_work(),改为只触发 assert,等于把 rtmutex 调度的契约收窄到"futex proxy 锁路径不需要 work flush"。

版本变化

  • v1→v2(Yao Kai):把 patch 1 的 scheduler helper 拆分替换成 rt_mutex_pre_schedule() / rt_mutex_post_schedule() 直接包住 rt_mutex_wait_proxy_lock();patch 2 注释扩展,解释为什么 saved-task 唤醒能覆盖 rcuwait 而不丢唤醒。
  • v2→v3:更新两份 commit message;删掉 patch 1 中关于"skipped schedule()"的误导注释;明确 rt_mutex_schedule() 要求先前必须调用过 rt_mutex_.*_schedule()
  • v3→v4:根据 PeterZ 意见重写为只触发 assert,新增 rt_mutex_futex_pre/post_schedule() 专供 futex proxy 锁使用;删掉 futex_lock_pi() 中的旧注解。
  • v4 后:patch 1(912edebe8501…)与 patch 2(a3b8d46fe40…)均已合入 tip: locking/urgent 分支。

一句话总结

本系列把 futex proxy 锁路径上的 sched_rt_mutex 注解从错位处搬正,并把 requeue-PI 完成阶段对已离开 waiter 的 rcuwait 唤醒掐掉,避免栈对象被释放后还被越权访问。