0/13 已展开

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 带来的不必要延迟。

旧流程的问题

  1. kfree_nolock() 只能释放 kmalloc_nolock() 路径分配的对象,普通 kmalloc()、kfence、large kmalloc 都被拒
  2. set_cpus_allowed_force() 在持有 p->pi_lock 时不能直接 kfree()(PREEMPT_RT 下不安全),被迫走 kfree_rcu() 引入额外延迟
  3. 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/5mm/slab: cleanup deferred free handlingmm/slub.c:删单次性局部变量、加注释、收紧 guard(preempt)+13/-11
2/5mm/slab, kfence: support kfence objects in kfree_nolock()mm/kfence/{core.c,kfence.h}:把 rcu_headllnode 做 union;mm/slub.c 增加 objects_kfence llist 和 defer_free_kfence()+59/-4
3/5mm/slab, kmemleak: handle kmemleak freeing in kfree_nolock()mm/kmemleak.c:新增 kmemleak_may_need_free()(trylock + __lookup_object()+85/-8
4/5mm/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/5sched: 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:风险与注意点

  1. sashiko AI 警告(Patch 2)[High]:double-free 一个 KFENCE 对象通过 kfree_nolock() 会引起 llist 循环或 rcu_head 覆写,把可检测 bug 变成不可恢复崩溃
  2. sashiko AI 警告(Patch 5)[High]:把 kfree_rcu() 换成 kfree_nolock() 在某些持锁路径可能触发 "Invalid wait context" lockdep 警告与 RT 死锁风险
  3. sashiko AI 警告(Patch 5)[Critical, pre-existing]relax_compatible_cpus_allowed_ptr()user_cpus_ptr 的 lockless 访问与 sched_setaffinity() 并发存在 UAF;与本 patch 无关但应一并跟进
  4. Catalin Marinas 关注(Patch 3):kmemleak 扫描时 kmemleak_lock 可能被 scan_block() 持续持有几分钟,期间几乎所有 kfree_nolock() 都得走 deferred;目前不显著,但如果 kfree_nolock() 普及就需观察
  5. NMI on !CONFIG_SMPkmemleak_may_need_free() 在单核 NMI 下 raw_spin_trylock() 永远成功会误判,代码已显式 if (!IS_ENABLED(CONFIG_SMP) && in_nmi()) return true;,需要测试覆盖
  6. 顺序差异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 延迟。