sched discussion
[PATCH RFC 0/5] allow kfree_nolock() handle kmalloc() objects
LLM 分析
mm/slab + sched: kfree_nolock() 扩展支持 kmalloc() 对象
系列概况
- 标题: [PATCH RFC 0/5] allow kfree_nolock() handle kmalloc() objects
- 作者: Vlastimil Babka (SUSE) vbabka@kernel.org
- 版本: RFC v1(明示目标7.4 合并窗口,基于 slab/for-7.3/kfree_rcu_nolock)
- 规模: 5 patches
- 修改文件: mm/slub.c、mm/kfence/core.c、mm/kfence/kfence.h、include/linux/kfence.h、mm/kmemleak.c、include/linux/kmemleak.h、kernel/sched/core.c、kernel/sched/sched.h
- 代码统计: 8 files changed, 215 insertions(+), 41 deletions(-)
- Message-ID: 20260807-kfree_nolock_kmalloc-v1-0-ba993cbf7a60@kernel.org
- 完整性: 13 封邮件(1 cover + 5 patches + 4 reply + 3 sashiko-bot 自动 review),其中 patch 1 已获 Hao Li 的 Reviewed-by,Catalin Marinas 对 patch 3 提出疑虑
补丁目的
把 kfree_nolock() 的适用范围从仅 kmalloc_nolock() 分配的对象扩展到任意 kmalloc() / kmem_cache_alloc() 对象(含 kfence、kmemleak 注册对象、large_kmalloc),让持锁上下文(包括调度器 set_cpus_allowed_force() 在持有 p->pi_lock 时)不必再退而求其次走 kfree_rcu(),从而省去 RCU grace period 带来的不必要延迟。
旧流程的问题
kfree_nolock()只能释放kmalloc_nolock()路径分配的对象,普通kmalloc()、kfence、large kmalloc 都被拒set_cpus_allowed_force()在持有p->pi_lock时不能直接kfree()(PREEMPT_RT 下不安全),被迫走kfree_rcu()引入额外延迟defer_free()局部变量冗余,注释粗糙,guard(preempt)范围过大
新流程
kfree_nolock()区分对象类型:常规 slab / kfence / large_kmalloc / 可能已注册 kmemleak- 必要时把对象挂到 per-CPU
deferred_percpu_work的 llist,由irq_work在安全上下文统一释放 - kmemleak 用
raw_spin_trylock_irqsave()试探;拿不到锁就假定已注册、推迟释放
caller (may hold raw_spinlock / PREEMPT_RT)
|
v
kfree_nolock(x)
|
+-----------+----------+-----------+-----------+
| | | | |
regular slab is_kfence large_kmalloc may_need (continue)
object address (first word kmemleak?
| | usable) |
defer_free() defer_free_ defer_free_ trylock
(freepointer _kfence() large_kmalloc() kmemleak_lock location) (kfence failed?
| metadata (first word |
| llnode) as llnode) yes -> defer
+-----------+-------------+--------------+
|
v
per-CPU deferred_percpu_work llist --irq_work--> deferred_percpu_work_fn
| | | |
normal kfence objects_by_rcu large_kmalloc
slab free (RCU sheaves) page free
Patch 概览
| # | 主题 | 关键改动 | 行数 |
|---|---|---|---|
| 1/5 | mm/slab: cleanup deferred free handling | mm/slub.c:删单次性局部变量、加注释、收紧 guard(preempt) | +13/-11 |
| 2/5 | mm/slab, kfence: support kfence objects in kfree_nolock() | mm/kfence/{core.c,kfence.h}:把 rcu_head 与 llnode 做 union;mm/slub.c 增加 objects_kfence llist 和 defer_free_kfence() | +59/-4 |
| 3/5 | mm/slab, kmemleak: handle kmemleak freeing in kfree_nolock() | mm/kmemleak.c:新增 kmemleak_may_need_free()(trylock + __lookup_object()) | +85/-8 |
| 4/5 | mm/slab: handle large_kmalloc objects in kfree_nolock() | mm/slub.c:新增 objects_large_kmalloc llist、defer_free_large_kmalloc()、free_frozen_pages_nolock() | +62/-12 |
| 5/5 | sched: use kfree_nolock() instead of kfree_rcu() | kernel/sched/{core.c,sched.h}:set_cpus_allowed_force() 改用 kfree_nolock() | +3/-13 |
关键实现
Patch 1 — 清理 defer_free():去掉 objs/objs_by_rcu/rcu_sheaves 等一次性局部变量,改为直接访问 &dpw->objects 等;写注释解释"把 llnode 写到 freepointer 位置"的语义;guard(preempt) 仅覆盖临界区。
Patch 2 — KFENCE 用 union(rcu_head, llnode):
struct kfence_metadata {
...
union {
struct rcu_head rcu_head; /* For delayed freeing. */
struct llist_node llnode; /* For kfree_nolock(). */
};
};
SLAB 不直接看到 KFENCE 内部,只通过新增的 kfence_obj_to_llnode() / kfence_llnode_to_obj() 转换。deferred_percpu_work_fn() 新增一段从 objects_kfence 取节点后调 __kfence_free()。选择 union 是因为 KFENCE 现有 rcu_head 正好能复用作 llnode,避免在 metadata 里再塞一个字段。
Patch 3 — kmemleak 试探:
bool __ref kmemleak_may_need_free(const void *ptr)
{
if (!raw_spin_trylock_irqsave(&kmemleak_lock, flags))
return true; /* 拿不到锁:宁可信其有 */
object = __lookup_object((unsigned long)ptr, 0, 0);
raw_spin_unlock_irqrestore(&kmemleak_lock, flags);
return !!object;
}
kfree_nolock() 在 kasan_slab_free() 后先做一次"是否需要 kmemleak 注册"判定;已注册则 goto defer 推迟,未注册则继续走 __slab_free()。
Patch 4 — large_kmalloc:新增 objects_large_kmalloc per-CPU llist头;defer_free_large_kmalloc() 把对象首字当 llnode(large kmalloc 无 ctor、不需要 TYPESAFE_BY_RCU 处理);free_frozen_pages_nolock() 替代 free_frozen_pages();free_large_kmalloc() 增加 free_flags 参数。
Patch 5 — 调度器替换:
/* kernel/sched/core.c: set_cpus_allowed_force() */
/* 旧:kfree_rcu((union cpumask_rcuhead *)ac.user_mask, rcu); */
kfree_nolock(ac.user_mask);
alloc_user_cpus_ptr() 不再为对齐 rcu_head 多分配一次;union cpumask_rcuhead 整个被移除。
类比
kfree_nolock() 像快递柜:你把要回收的"包裹"(内存对象)塞进去时,柜子不打扰正在开会的工作人员(持锁上下文),由后台分拣员(irq_work 派发的 deferred_percpu_work_fn)按安全节奏把它们送回仓库。
扩展支持 kmalloc() 后,柜子开始接受"任意来源"的包裹:
- 普通包裹直接送走(
__slab_free()路径) - 印有 KFENCE 标签的走专门窗口(
kfence_llnode,复用 rcu_head 字段) - 大件包裹(large kmalloc)按"冷冻车"流程走(
free_frozen_pages_nolock) - 已被 kmemleak 登记的包裹要先撤登记(
kmemleak_free())
调度器之前用 kfree_rcu() 是把包裹交给"RCU 邮局",必须等一个 grace period 才能处理;现在改用 kfree_nolock(),多数情况下当场销毁,体验上等同把邮局替换成楼下快递柜。
Highlight:风险与注意点
- sashiko AI 警告(Patch 2)[High]:double-free 一个 KFENCE 对象通过
kfree_nolock()会引起 llist 循环或rcu_head覆写,把可检测 bug 变成不可恢复崩溃 - sashiko AI 警告(Patch 5)[High]:把
kfree_rcu()换成kfree_nolock()在某些持锁路径可能触发 "Invalid wait context" lockdep 警告与 RT 死锁风险 - sashiko AI 警告(Patch 5)[Critical, pre-existing]:
relax_compatible_cpus_allowed_ptr()对user_cpus_ptr的 lockless 访问与sched_setaffinity()并发存在 UAF;与本 patch 无关但应一并跟进 - Catalin Marinas 关注(Patch 3):kmemleak 扫描时
kmemleak_lock可能被scan_block()持续持有几分钟,期间几乎所有kfree_nolock()都得走 deferred;目前不显著,但如果kfree_nolock()普及就需观察 - NMI on !CONFIG_SMP:
kmemleak_may_need_free()在单核 NMI 下raw_spin_trylock()永远成功会误判,代码已显式if (!IS_ENABLED(CONFIG_SMP) && in_nmi()) return true;,需要测试覆盖 - 顺序差异:
kfree_nolock()中 kmsan/kasan 处理先于 kmemleak,跟kfree()不同;需保证 untagged 指针参与 kmemleak 查找(__lookup_object()自身会重置 tag)
版本变化
本系列只此 v1 RFC,作者明确"could target 7.4 and definitely not earlier merge window",无 vN→vN+1 对比;后续主要观察 kmemleak deferral 是否成为热路径瓶颈,以及 kfence union{rcu_head, llnode} 在其他 kfence 使用点的兼容性。
一句话总结
把 kfree_nolock() 从"仅限 kmalloc_nolock 内部用"扩展成"任意 kmalloc() 对象都能释放",代价是 kfence / kmemleak 注册对象 / large_kmalloc 会被推迟到 irq_work;调度器借此把 kfree_rcu() 换掉,避免 RCU grace period 延迟。