sched discussion
[PATCH 1/2] futex/requeue: Fix rtmutex schedule preparation for requeue PI
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.h、kernel/futex/requeue.c、kernel/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 路径上的两个独立缺陷:
- 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。 futex_requeue_pi_complete()把状态推到Q_REQUEUE_PI_LOCKED后,waiter 可能立即出栈,而rcuwait_wake_up()仍读取q->requeue_wait.task,造成 KASANslab-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 1:
futex/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 2:
futex/requeue: Prevent rcuwait use-after-free during requeue PI,futex_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 拆分。