bpf: Invalidate RCU pointers after final spin unlock
💡 一句话总结
BPF verifier 把"持锁期间"视为隐式 RCU 读侧区(in_rcu_cs() 含 active_locks),因此持锁时从 map 里读出的 kptr 会被标记为 MEM_RCU;在 sleepable 程序里释放最后一把锁会结束唯一 RCU 上下文,但旧 verifier 仍允许该指针继续使用,造成可被触发的 task_struct use-after-free。本补丁在 process_spin_lock() 解锁路径记录解锁前 RCU 状态,当解锁使 in_rcu_cs() 从 true 翻转为 false 时,调用已有的 invalidate_rcu_protected_refs() 把 MEM_RCU 指针降级为 PTR_UNTRUSTED——这是一次 verifier 逻辑健全性(soundness)修复,而非性能优化。
📋 补丁基本信息
| 项目 | 内容 |
|---|---|
| 补丁类型 | 正确性修复(verifier 健全性,防 use-after-free) |
| 状态 | In Review(v2,定向 bpf-next) |
| 当前版本 | v2 1/2 · lore 链接 |
| 版本演进 | v1 [PATCH bpf](08-02,单补丁 5 文件 +91 行)→ v2 [PATCH bpf-next 1/2](08-03,拆分 + 重定向,verifier 只 +5 行) |
| 作者 | Ning Ding(dingning04) |
| Fixes | 5861d1e8dbc4("bpf: Allow bpf_spin_{lock,unlock} in sleepable progs") |
| 提交日期 | 2026-08-03 |
| 改动范围 | v2 1/2:1 文件(kernel/bpf/verifier.c),+5 行;系列共 +5/+86(含 selftests) |
| 核心函数 | process_spin_lock() / in_rcu_cs() / invalidate_rcu_protected_refs() |
📊 速览卡片
🎯 解决什么问题
in_rcu_cs() 判定当前是否处于 RCU 读侧区(本机 7.2-rc6 源码):
return active_rcu_locks || active_locks || !in_sleepable(env);
即:显式 bpf_rcu_read_lock 期间、持有一把 verifier 跟踪的 spin lock 期间、以及整个非 sleepable 程序执行期间,都被视为 RCU 保护上下文。因此持锁时从 map value 读出的 kptr(如 task_struct __kptr)会被标记 MEM_RCU,verifier 相信它受 RCU 保护、可安全解引用。
process_spin_lock() 的解锁路径此前只调用 invalidate_non_owning_refs()——它把 non-owning ref(如链表/红黑树节点指针)标记失效,但保留 MEM_RCU 引用不动。这在对非 sleepable 程序是对的(整个调用都在隐式 RCU 里);但对 sleepable 程序,如果释放的是最后一把锁,active_locks 归零、又没有显式 RCU 锁、又 sleepable,in_rcu_cs() 会从 true 翻转为 false——RCU 保护已结束,而 verifier 仍把 MEM_RCU 指针当有效引用放行。
bpf_spin_unlock 之后,可与"map kptr 被删除 + bpf_task_release()"竞争,在 __bpf_get_task_stack() 中触发 task_struct use-after-free。这属于 verifier 未能捕获的"RCU 保护期后使用"漏洞,与显式 RCU 章节已有防护形成不对称。
🧩 核心机制
核心是把"释放最后一层 RCU 上下文"时对 MEM_RCU 指针的失效处理,补充到 spin-unlock 路径上,与显式 bpf_rcu_read_unlock 路径(主线上已有)对称。
| 步骤 | 代码 | 语义 |
|---|---|---|
| ① 记录解锁前状态 | was_in_rcu_cs = in_rcu_cs(env); | 在 release_lock_state() 之前采样——因为解锁会递减 active_locks,之后就无法回看"解锁前是否受保护" |
| ② 执行解锁 | release_lock_state(cur, ...) | active_locks--,可能让 in_rcu_cs() 变 false |
| ③ 上下文退出则失效 | if (was_in_rcu_cs && !in_rcu_cs(env)) invalidate_rcu_protected_refs(env); | 仅在"RCU 保护从有到无"时,把所有 MEM_RCU 寄存器降级为 PTR_UNTRUSTED,此后对它们的 bpf_task_acquire / 解引用会被 verifier 拒绝 |
invalidate_rcu_protected_refs() 并非新函数,主线早已存在(由 bcfcb15fde94 "bpf: Unify release handling for helpers and kfuncs" 引入并接入显式 bpf_rcu_read_unlock 路径);本补丁只是把它复用到 spin-unlock 路径,改动极小。
!in_sleepable(env) 恒真 → in_rcu_cs() 恒 true → 解锁后 !in_rcu_cs(env) 恒 false → 永不触发失效,指针保持有效(正确)。显式 RCU 读侧区内:
active_rcu_locks > 0 → 解锁后 in_rcu_cs() 仍 true → 不触发失效,指针仍受保护(正确)。sleepable 且无其他 RCU 上下文:解锁使
active_locks 归零 → in_rcu_cs() 翻转为 false → 触发失效(这是唯一被收紧、也是唯一需要收紧的场景)。
🔬 关键代码
以下为 v2 1/2 对 kernel/bpf/verifier.c 的完整改动(+5 行)。
@@ -206,6 +206,7 @@ static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, int par
static int release_reference_nomark(struct bpf_verifier_state *state, int id);
static int release_reference(struct bpf_verifier_env *env, int id);
static void invalidate_non_owning_refs(struct bpf_verifier_env *env);
+static void invalidate_rcu_protected_refs(struct bpf_verifier_env *env);
static bool in_rbtree_lock_required_cb(struct bpf_verifier_env *env);
static bool is_tracing_prog_type(enum bpf_prog_type type);▲ 前置声明:让后面的 process_spin_lock() 能调用定义在文件后部的 invalidate_rcu_protected_refs()(该函数在 7.2-rc6 中位于第 ~9016 行,供显式 RCU 解锁使用)。
@@ -7165,6 +7166,7 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
return err;
}
} else {
+ bool was_in_rcu_cs;
void *ptr;
int type;▲ 在 unlock 分支新增局部变量 was_in_rcu_cs,用于回看"释放锁之前的 RCU 状态"。
@@ -7192,10 +7194,13 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
verbose(env, "%s_unlock cannot be out of order\n", lock_str);
return -EINVAL;
}
+ was_in_rcu_cs = in_rcu_cs(env);
if (release_lock_state(cur, type, reg->id, ptr)) {
verbose(env, "%s_unlock of different lock\n", lock_str);
return -EINVAL;
}
+ if (was_in_rcu_cs && !in_rcu_cs(env))
+ invalidate_rcu_protected_refs(env);
invalidate_non_owning_refs(env);
}▲ 核心三行:was_in_rcu_cs = in_rcu_cs(env) 必须在 release_lock_state()(它递减 active_locks)之前采样;解锁成功后若检测到 RCU 上下文由 true 转 false,则失效所有 MEM_RCU 指针,最后照旧失效 non-owning refs。顺序上紧贴 invalidate_non_owning_refs,保证"失效不遗漏、不误伤"。
task_kfunc_acquire_after_final_spin_unlock(sleepable fentry.s):持锁读 task_kptr_lock_map 里的 task_struct __kptr,解锁后调用 bpf_task_acquire(task),verifier 必须报错 "R1 must be a rcu pointer"。正向对照 1
task_kfunc_acquire_after_spin_unlock_non_sleepable(非 sleepable fentry):同样模式必须被接受(整个调用隐式 RCU)。正向对照 2
task_kfunc_acquire_after_spin_unlock_explicit_rcu:把解锁包在 bpf_rcu_read_lock/unlock 内,必须被接受(显式 RCU 仍保护指针)。
⚡ 性能影响
🔄 方案演进 + 讨论焦点
20260802231248.2781334-1,[PATCH bpf],08-02):单补丁,verifier 修复 + selftests 一起,5 文件 +91 行。Kumar Kartikeya Dwivedi(memxor)评审 v1:"It makes sense, but split the kernel side fix and selftests into two separate patches. The list has several examples. Also, I don't think this is as serious, so please target bpf-next in the respin."(标记
pw-bot: cr,Change Requested)。v2(
20260803112615.3362122,[PATCH bpf-next v2 1/2 + 2/2],08-03):完全按评审意见重发——拆成 verifier 修复(1/2,+5 行)与 selftests(2/2,+86 行)两补丁,目标树从 bpf 改为 bpf-next。
- 拆分规范:Kumar 要求内核修复与测试分补丁,v2 已落实——这是 bpf 社区的惯例。
- 严重度判断与目标树:作者 v1 发到 bpf(含 stable 的修复线),Kumar 认为"没那么严重",要求重发到 bpf-next。说明这是一个 verifier 边界漏洞、尚无在野利用证据(解读(AI 分析))。
- 失效语义的邻域讨论:同一函数
invalidate_rcu_protected_refs()的"是否应顺带清除PTR_MAYBE_NULL"在社区有并行讨论——Yiyang Chen 的 "bpf: Preserve nullable RCU pointer state on unlock" 主张只清MEM_RCU而保留PTR_MAYBE_NULL,本补丁调用的主线版本目前是两者一起清。两者若都合入,PTR_MAYBE_NULL的处理需保持一致(解读(AI 分析))。
⚠️ 风险与局限
MEM_RCU 指针变 PTR_UNTRUSTED,会拒绝一部分"此前能加载"的 sleepable 程序。这是修复 UAF 的必然代价;若真有程序依赖该行为,说明其本身就存在 UAF 隐患(解读(AI 分析))。范围窄:仅影响 sleepable + spin-lock 组合场景;非 sleepable 与显式 RCU 读侧区完全不受影响。
失效语义需与并行补丁对齐:
PTR_MAYBE_NULL 的清除与否存在社区讨论,若后续补丁改变 invalidate_rcu_protected_refs() 语义,本补丁的行为会随动。Fixes 定位:指向 5861d1e8dbc4(允许 sleepable 用 spin_lock)合理——正是该提交让"持锁=隐式 RCU"进入 sleepable 程序,漏洞才可达;并非
in_rcu_cs() 或失效函数本身引入(解读(AI 分析))。
🔗 交叉引用
- Fixes:
5861d1e8dbc4"bpf: Allow bpf_spin_{lock,unlock} in sleepable progs"——解除 sleepable+spin_lock 禁令,间接引入本漏洞。 - 失效函数来源:
bcfcb15fde94"bpf: Unify release handling for helpers and kfuncs"——引入invalidate_rcu_protected_refs()并接入显式bpf_rcu_read_unlock路径。 - 同作者邻域系列:Ning Ding 另一组 v3 "[PATCH bpf v3 1/4] bpf: Keep refcount_acquire nullable for borrowed RCU kptrs" 及 "[PATCH bpf v3 3/4] bpf: Reject untrusted allocated-object pointers"——同样处理"借用 RCU kptr 在保护结束后被误用",说明作者在系统性收紧 verifier 的 RCU 借用边界。
- 并行语义讨论:Yiyang Chen "bpf: Preserve nullable RCU pointer state on unlock"——讨论
invalidate_rcu_protected_refs()对PTR_MAYBE_NULL的处理。
✅ 关键洞察
- 发现:verifier 的 RCU 上下文模型存在"入口跟踪、出口漏算"——
in_rcu_cs()把持锁视为 RCU 读侧区,但 spin-unlock 路径只清 non-owning refs,未在"最后一个 RCU 上下文退出"时清MEM_RCU;这是典型的上下文转换(context-transition)漏洞。 - 机制:解锁前采样
was_in_rcu_cs,解锁后若in_rcu_cs()由 true 转 false,复用已有的invalidate_rcu_protected_refs()把所有MEM_RCU降级为PTR_UNTRUSTED——与显式bpf_rcu_read_unlock路径完全对称,改动仅 +5 行。 - 边界:非 sleepable 程序(调用级隐式 RCU)与显式 RCU 读侧区内的解锁不受影响;只有"sleepable 且释放最后一把锁"被收紧。
- 性质判断:这是正确性/安全修复,非性能补丁;无基准数据,运行时零开销。对本日报的性能视角,价值在于说明"BPF verifier 健全性修复是日常内核工作的重要组成",并提示 sleepable BPF 程序作者注意持锁读出的 kptr 生命周期。
本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。