0/15 已展开

LLM 分析

futex/requeue:修复 PREEMPT_RT 上的 requeue PI 竞争与 UAF

系列概况

  • 标题:[PATCH 0/2] futex/requeue: Fix requeue PI races
  • 作者:Yao Kai yaokai34@huawei.com
  • 版本:v1(cover letter 标注 [PATCH 0/2],无后续版本号)
  • 规模:2 个补丁,3 个文件变更
  • 修改文件include/linux/sched/rt.hkernel/futex/requeue.ckernel/sched/core.c
  • 代码统计:60 行新增,11 行删除(Patch 1 自报 53/9,Patch 2 自报 7/2)
  • Message-ID:cover letter 20260717084922.4153317-1-yaokai34@huawei.com;Patch 1 ...4153317-2-...;Patch 2 ...4153317-3-...
  • 完整性:完整,cover letter 列出两 patch 的 Fixes tag、stable Cc 以及测试矩阵(arm64 defconfig + W=1、PREEMPT_RT + KASAN/PROVE_LOCKING/DEBUG_RT_MUTEXES、checkpatch、selftest 复现器)

补丁目的

修复 PREEMPT_RT 上 FUTEX_WAIT_REQUEUE_PI / FUTEX_CMP_REQUEUE_PI 路径上的两个独立缺陷:

  1. waiter 在 requeue 完成后直接进入 rt_mutex_wait_proxy_lock(),但 sched_rt_mutex 标志位未置位、blk_flush_plug 未执行、worker 通知未发出,触发 WARNING: CPU: 0 PID: 293 at kernel/sched/core.c:7606
  2. futex_requeue_pi_complete() 把状态推到 Q_REQUEUE_PI_LOCKED 后,waiter 可能立即出栈,而 rcuwait_wake_up() 仍读取 q->requeue_wait.task,造成 KASAN slab-out-of-bounds

旧流程的问题

futex_do_wait()q.list 被临时取出(requeue 路径上的 plist_del + plist_add)时,把这次操作误判成"已被唤醒",于是 schedule() 被跳过。waiter 紧接着被 requeue 端把 rt_waiter 入队并安装 pi_blocked_on,进入 rt_mutex_schedule()current->sched_rt_mutex 仍为 0,blk_flush_plug 也没有机会运行——对 workqueue / io-wq 用户而言就是"worker 要睡了却不通知"。

另一个独立竞态:requeue_pi_wake_futex() 在发布 Q_REQUEUE_PI_LOCKED 之前会 READ_ONCE(q->task) 保存 task 指针并随后 wake_up_state,但 futex_requeue_pi_complete() 仍然在 LOCKED 状态下调用 rcuwait_wake_up(&q->requeue_wait),后者从已可能出栈的 q 中读取 requeue_wait.task 传给 try_to_wake_up()

新流程

  • sched_submit_work() 拆成 _notifiers(wq / io-wq 通知)和 _plug(flush block plug)两部分,并新增两个接口:
    • rt_mutex_pre_schedule_flush_plug():仅 flush plug,用于 q 还未发布、其它任务还不能据此把我们入队的时刻;
    • rt_mutex_pre_schedule_pi_blocked():置 sched_rt_mutex、做 notifier 通知但不 flush plug,用于已经 pi_blocked_on 的时刻。
  • futex_wait_requeue_pi()futex_queue(&q) 之前先 flush plug;看到 Q_REQUEUE_PI_DONE 之后再走 pi_blocked 入口;最后用 rt_mutex_post_schedule() 还原状态。
  • futex_requeue_pi_complete()new == Q_REQUEUE_PI_LOCKED 时不再调用 rcuwait_wake_up()

Patch 概览

  • Patch 1futex/requeue: Fix rtmutex schedule preparation for requeue PI,拆 sched_submit_work,新增两个 helper,并在 futex_wait_requeue_pi() 三处调用。Fixes: d14f9e930b90("locking/rtmutex: Use rt_mutex specific scheduler helpers")。
  • Patch 2futex/requeue: Prevent rcuwait use-after-free during requeue PIfutex_requeue_pi_complete()LOCKED 状态下跳过 rcuwait_wake_up()。Fixes: 07d91ef510fb1("futex: Prevent requeue_pi() lock nesting issue on RT")。

关键实现

/* kernel/sched/core.c - 拆分后的两条准备路径 */
void rt_mutex_pre_schedule_flush_plug(void)
{
    lockdep_assert(!current->pi_blocked_on);
    lock_map_acquire_try(&sched_map);
    sched_submit_work_plug(current);      /* 仅 flush plug */
    lock_map_release(&sched_map);
}

void rt_mutex_pre_schedule_pi_blocked(void)
{
    lockdep_assert(current->pi_blocked_on);
    lockdep_assert(!fetch_and_set(current->sched_rt_mutex, 1));
    lock_map_acquire_try(&sched_map);
    sched_submit_work_notifiers(current); /* 仅通知 wq / io-wq */
    lock_map_release(&sched_map);
}
/* kernel/futex/requeue.c - Patch 1 的三个插入点 */
rt_mutex_pre_schedule_flush_plug();     /* q 还不可见,先 flush plug */
futex_queue(&q);

case Q_REQUEUE_PI_DONE:
    pi_mutex = &q.pi_state->pi_mutex;
    rt_mutex_pre_schedule_pi_blocked(); /* 进入 rtmutex schedule 状态 */
    ret = rt_mutex_wait_proxy_lock(pi_mutex, to, &rt_waiter);
    /* ... */
    rt_mutex_post_schedule();
/* kernel/futex/requeue.c - Patch 2 跳过 LOCKED 状态的 rcuwait wake */
if (unlikely(old == Q_REQUEUE_PI_WAIT) && new != Q_REQUEUE_PI_LOCKED)
    rcuwait_wake_up(&q->requeue_wait);

旧 vs 新流程图

OLD (Patch 1)
  waiter                          requeue task
  futex_queue(&q)  --visible-->   enqueue rt_waiter
  futex_do_wait()                 install pi_blocked_on
  plist_node_empty() == true      requeue_futex(): plist_del/add
  -> skip schedule()              IN_PROGRESS -> DONE
  rt_mutex_schedule()  [!] sched_rt_mutex == 0, plug not flushed

NEW (Patch 1)
  waiter                          requeue task
  pre_schedule_flush_plug()       (q not published yet)
  futex_queue(&q)
  futex_do_wait()                 enqueue rt_waiter + pi_blocked_on
  ...                             IN_PROGRESS -> DONE
  case Q_REQUEUE_PI_DONE:
    pre_schedule_pi_blocked()     <- set flag + notify only, no flush
    rt_mutex_wait_proxy_lock()
    rt_mutex_post_schedule()
OLD (Patch 2)
  T1 waiter                  T2 requeue task
  ...                        requeue_pi_wake_futex(): task = READ_ONCE(q->task)
  observe LOCKED <---------- publish Q_REQUEUE_PI_LOCKED
  return; q goes out of scope
                             rcuwait_wake_up(&q->requeue_wait)  [BOOM: stale]

NEW (Patch 2)
  T1 waiter                  T2 requeue task
  ...                        requeue_pi_wake_futex(): task = READ_ONCE(q->task)
  observe LOCKED <---------- publish Q_REQUEUE_PI_LOCKED
  return; q gone, task already saved
                             skip rcuwait_wake_up()
                             wake_up_state(task, TASK_NORMAL)   [OK]

类比

futex_q 想成餐桌上一张"我还没吃完"的占座牌。旧流程里,requeue 服务员看到桌上一瞬间没牌就以为客人走了、于是去厨房叫菜(schedule 被跳过),等真要上 rtmutex 这道主菜时才发现订单还没递出去(sched_rt_mutex 没置、plug 没 flush),厨房乱套。补丁把"翻桌牌"和"下单"拆开:先在占座牌还压在桌上时把厨房里的待办(plug)清掉,再在客人被转交给新餐桌后才正式下单。客人走的时候不再去翻桌牌,而是直接喊名字(提前保存的 task 指针),避免碰已经被收走的桌牌。

Highlight:风险与注意点

  • 递归风险:审计路径 pre_schedule -> blk_flush_plug -> drbd_unplug -> spin_lock_irq -> rtlock_slowlock -> task_blocks_on_rt_mutex 理论存在,作者据此拒绝把 rt_mutex_pre_schedule() 整体挪到 Q_REQUEUE_PI_DONE 之后;但 Sebastian 与作者都确认这条路径在 FUTEX_WAIT_REQUEUE_PI 上不可达(无活 plug、无 worker flag),因此 Patch 1 的复杂拆分可能是过度设计。
  • maintainer 倾向更小方案:Sebastian 建议直接在 Q_REQUEUE_PI_DONE 分支包一对 rt_mutex_pre_schedule() / rt_mutex_post_schedule(),其测试同样消除 WARNING,最终讨论收敛到"接受这种写法 + 加一句注释"。
  • post_schedule 位置lockdep_assert(current->sched_rt_mutex) 要求三段式严格成对;讨论中一度以为 spin_lock() 会自己跑三段式,最后澄清 PREEMPT_RT 的 spin_lock()rt_spin_lock -> rtlock_lock -> schedule_rtlock,不涉及该状态,所以 post_schedule() 紧跟 rt_mutex_wait_proxy_lock() 或推到 debug_rt_mutex_free_waiter() 之后都可接受。
  • LOCKED 唤醒语义:跳过 rcuwait_wake_up() 后,T1 的唤醒完全依赖 requeue_pi_wake_futex() 里的 wake_up_state(task, TASK_NORMAL) 兜底;Sebastian 认为这层"外层唤醒救场"的语义必须写注释,否则读者会怀疑漏醒。
  • stable 回溯:两 patch 均 Cc: stable,对 PREEMPT_RT 用户影响明显,回溯时需注意 Patch 1 若改成小方案则与已发布版本不同。

版本变化

本系列只有 v1,尚未发出 v2。讨论方向:Patch 1 很可能从"拆分 sched_submit_work"简化为"在 Q_REQUEUE_PI_DONE 分支直接使用 rt_mutex_pre_schedule() / rt_mutex_post_schedule()";Patch 2 方案被接受但需补注释说明 wake_up_state() 为何足够。

与其他相关 patch 系列的关联

  • 直接建立在 d14f9e930b90("locking/rtmutex: Use rt_mutex specific scheduler helpers")引入的 rtmutex 专用调度 helper 之上。
  • 07d91ef510fb1("futex: Prevent requeue_pi() lock nesting issue on RT")引入的 requeue_state 状态机及 PREEMPT_RT rcuwait 同步机制相关。
  • 参照 futex_lock_pi()rt_mutex_post_schedule() 的延迟摆放惯例。

一句话总结

该系列修复 PREEMPT_RT 下 requeue PI 的两个竞态——waiter 跳过 schedule() 后带着未置位的 sched_rt_mutex 进入 rt_mutex_schedule(),以及 Q_REQUEUE_PI_LOCKED 发布后 rcuwait_wake_up() 触碰已出栈的栈上 futex_q——评审倾向用更小的 pre/post_schedule 包裹替代 Patch 1 的 sched_submit_work 拆分。