sched discussion
[PATCH v4 0/2] futex: Address two futex-requeue-pi issues
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.h、kernel/futex/pi.c、kernel/futex/requeue.c、kernel/locking/rtmutex_api.c、kernel/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 路径上两个长期被忽视的问题:
- 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 指出)。 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_LOCKED时rcuwait_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 唤醒掐掉,避免栈对象被释放后还被越权访问。