0/8 已展开

LLM 分析

futex:futex_pivot_pending() 中修复 might_sleep() 警告

系列概况

  • 标题: [PATCH] futex: Fix might_sleep() warning in futex_pivot_pending()
  • 作者: Yao Kai yaokai34@huawei.com(由 syzbot 转交,AI 协助起草 Gemini 3.6-flash / 3.1-pro-preview)
  • 版本: v1(单封补丁)
  • 规模: 仅修改 1 个文件,5 处 hunk
  • 修改文件: kernel/futex/core.c
  • 代码统计: 净增约 10 行(移除 1 行 guard(mutex),新增 RCU 读侧 + smp_rmb() + WRITE_ONCE 包装)
  • Message-ID: 515ea00f-a081-4b9a-bcb3-f5517fd4e565@mail.kernel.org
  • 完整性: 补丁完整,含 Fixes: 8e7ff730dd96Assisted-byReported-by syzbot、Signed-off-by、base-commit

补丁目的

消除 syzbot 报告的 might_sleep() 警告。

前一个 commit 8e7ff730dd96("futex: Fix race in futex_pivot_pending() during private hash resize")把 futex_pivot_pending() 改成持 mmph->lock 来消除 race。但 futex_pivot_pending()futex_hash_allocate() 内作为 wait_var_event() 的条件 callback 调用,而 wait_var_event() 在求值条件前会把 task 状态置为 TASK_UNINTERRUPTIBLE,此时再调 mutex_lock() 违反 sleeping 规则:

WARNING: kernel/sched/core.c:9124 at __might_sleep+0x92/0xf0
Call Trace:
 __mutex_lock_common kernel/locking/mutex.c:623
 futex_pivot_pending kernel/futex/core.c:1789
 futex_hash_allocate+0x7fb/0xf00 kernel/futex/core.c:1872
 __do_sys_prctl kernel/sys.c:2885

旧流程的问题

futex_pivot_pending() 在 hash resize 期间需要判断私有 hash 是否已被替换成新 hash。前一版 fix 用 mmph->mutex 保护,但 mutex 在 waitqueue 条件 callback 内调用是非法的,且任何 sleeping 调用都不允许在 !TASK_RUNNING 状态下发生。

新流程

回退到无锁实现:进入 RCU 读侧,先 rcu_dereference(hash)、再 smp_rmb()、再 READ_ONCE(hash_new),借助 Message Passing (MP) pattern 保证正确性而不阻塞:

guard(rcu)();
fph = rcu_dereference(mmph->hash);
/*
 * 若看到新 hash,则一定看到 hash_new 被清零。
 * 与 __futex_pivot_hash() 中的 rcu_assign_pointer() 配对。
 */
smp_rmb();
if (!READ_ONCE(mmph->hash_new))
    return true;
return futex_ref_is_dead(fph);

写侧 __futex_pivot_hash() 通过 rcu_assign_pointer() 提供 release 屏障,reader 端 smp_rmb() 配对,保证 MP 关系。mm->futex.phash.hash_new 在所有写点都用 WRITE_ONCE() 包装,防止编译器把 store 优化掉。

关键实现

  1. RCU 生命周期:旧 hash 通过 kvfree_rcu() 释放,reader 处于 RCU 读侧临界区时旧 hash 内存一定有效。
  2. 屏障配对smp_rmb() 与写侧 rcu_assign_pointer() 的隐含 release barrier 严格配对,缺一会失效。
  3. write 侧 WRITE_ONCE__futex_pivot_hash()futex_pivot_hash()futex_hash_allocate() 多个写点统一改为 WRITE_ONCE(hash_new, ...)
  4. lost-wakeup 修补:Peter Zijlstra 在 review 中指出 add_wait_queue()wake_up_var() 之间仍存在窗口,需要在 add_wait_queue 之后再补一个 smp_mb(),最终采纳 smp_mb__after_spinlock()
  5. while 化简MAX_SCHEDULE_TIMEOUT 永不返回 0,可把 wait_woken() 返回值检查从 while 条件中移除。

类比

把 hash pivot 想象成「搬家换门牌号」。读者拿着旧门牌去敲门,必须先确认公告牌确实换了。rcu_dereference(hash) 是「看公告牌当前挂着哪一块」;smp_rmb() 是「保证我看到新牌子时,旧牌子一定已经被撕掉,绝不会看到新牌子挂着、旧公告还在」的视觉确认;旧房子通过 kvfree_rcu() 延迟拆除,住客在 RCU 临界区内绝不会被扫地出门。

Highlight:风险与注意点

  • Lost-wakeup 窗口:仅靠 add_wait_queue() 内部的 UNLOCK 不够,PowerPC 上的 RCtso 弱序需要补 smp_mb__after_spinlock()
  • 屏障对称性:单独在 reader 一侧加 smp_rmb() 而不与 rcu_assign_pointer() release 配对,整套推理立即失效。
  • kvfree_rcu 依赖:reader 安全完全建立在 kvfree_rcu() 上;若将来切换为同步 kfree() 释放,立即 UAF。
  • wait_woken 优化MAX_SCHEDULE_TIMEOUT 路径下返回值总非 0,可去掉多余的 && wait_woken(...) 检查。
  • AI 自动起草风险:当前 patch 由 Gemini 自动生成、人类作者签字,需要更多真实 workload 验证 RCU 读侧开销。

版本变化

  • v1:单封补丁,把 futex_pivot_pending() 改回 RCU + smp_rmb() 无锁实现,对应 Fixes: 8e7ff730dd96
  • review 阶段:Peter 指出 lost-wakeup 风险并建议加 smp_mb();Yao 改用 smp_mb__after_spinlock(),并在最后两封邮件中由 Peter 接受这一选择。讨论尚未发布 v2,正式结论以新补丁为准。

一句话总结

futex_pivot_pending() 从持锁版本回退到 RCU + smp_rmb() 无锁实现消掉 might_sleep(),再补一道 smp_mb__after_spinlock() 关上 wait_var_event 内部的 lost-wakeup 窗口。

                       pivot 完成判定(reader / writer 同步)

T1 (waiter via prctl)                       T2 (hasher / ref-put path)
----------------------------------         ------------------------------------
add_wait_queue(wq_head, &entry)            futex_ref_put()           (atomic full mb)
  STORE  wq_entry                          wake_up_var()
  UNLOCK  wq_head->lock                       └ lockless waitqueue_active() check
smp_mb__after_spinlock()                   rcu_assign_pointer(hash,new)  [RELEASE]
  ↕ pairs with futex_ref_put mb           WRITE_ONCE(hash_new, NULL)
guard(rcu)()                                  (old hash released via kvfree_rcu)
rcu_dereference(hash) ---+
smp_rmb()                 |  → 看到 hash==new ⇒ hash_new 必为 NULL
READ_ONCE(hash_new)  ----+    ⇒ return true(pivot 完成,退出 wait)
                           → 否则 futex_ref_is_dead(old)?
                                true  ⇒ return true
                                false ⇒ wait_woken(... MAX_SCHEDULE_TIMEOUT)