bpf: Invalidate RCU pointers after final spin unlock

eBPF · verifier 正确性修复 · sleepable 程序 spin-lock 隐式 RCU 上下文

💡 一句话总结

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)
Fixes5861d1e8dbc4("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()

📊 速览卡片

核心机制
上下文退出失效
优化目标
正确性/防 UAF
适用场景
sleepable+spin_lock
实测提升
无(正确性)

🎯 解决什么问题

背景:verifier 把"持锁"当作隐式 RCU 保护
BPF verifier 的 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 保护、可安全解引用。
系统层面:解锁只清 non-owning,不清 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 指针当有效引用放行。
场景层面:可触发的 task_struct use-after-free
作者确认:在 sleepable 程序里把 task kptr 从锁保护的 map 中读出并保留到最后一次 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 路径,改动极小。
为什么这样不会误伤(三种上下文逐一核对)
非 sleepable 程序:!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,保证"失效不遗漏、不误伤"。

配套 selftests(v2 2/2,+86 行)
负向用例 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 仍保护指针)。

⚡ 性能影响

实测数据
未提供。补丁提交说明与 cover letter 均未附带任何基准数据。这符合补丁性质:verifier 校验期(load 时)的分析逻辑修复,运行时(执行期)只改变"哪些程序能被加载/被拒绝",不引入任何指令级开销。对已被正确编写的 sleepable 程序,加载行为不变;唯一"代价"是此前能通过、实则存在 UAF 风险的程序会被拒绝加载——这是有意的健全性收紧(解读(AI 分析))。
影响面:运行时零开销 · 仅校验期行为变更

🔄 方案演进 + 讨论焦点

v1 → v2:评审驱动的拆分与重定向
v1(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 分析))。

⚠️ 风险与局限

潜在回归 / 边界
行为收紧:把解锁后离开 RCU 上下文的 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 分析))。
严重度:LOW-MED · verifier 健全性收紧 · 评审建议走 bpf-next

🔗 交叉引用

  • 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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。