BPF 程序能否安全读取属于另一个进程的用户态内存?
简短回答: BPF 程序无法用 bpf_probe_read_user* 这类 helper 直接读取另一个任务的用户态内存——这些 helper 通过 copy_from_user_nofault() 读取的是当前任务的用户态地址空间,一旦访问出错就静默返回零填充的数据。要在 BPF 里检查别的任务的用户态状态,有两条受支持的路径:要么在目标任务自己的上下文里跑(uprobe 打在该任务的用户库上、fentry/fexit 打在该任务的内核入口上),让探针本身落在那个任务上,然后 bpf_probe_read_user() 自然读它的内存;要么用 bpf_task_storage_*(按任意 task_struct 作为 key)去读该任务自己存进 BPF_MAP_TYPE_TASK_STORAGE 的每任务数据。当前 BPF helper 集合里并没有“从一个与目标任务不匹配的上下文读取任意其他任务用户内存”的单一 helper。
为什么 bpf_probe_read_user 返回全零而不是错误
bpf_probe_read_user() 是故意为容错的。手册页把它描述为“安全地尝试从用户态地址 unsafe_ptr 读取 size 字节”,成功返回 0,失败返回负错误。但这里的“出错”指当前任务的内存上下文里该地址不可读。看 kernel/trace/bpf_trace.c 里的实现:
static __always_inline int
bpf_probe_read_user_common(void *dst, u32 size, const void __user *unsafe_ptr)
{
int ret;
ret = copy_from_user_nofault(dst, unsafe_ptr, size);
if (unlikely(ret < 0))
memset(dst, 0, size);
return ret;
}任何一次访问故障,copy_from_user_nofault 都返回 -EFAULT,helper 随即把目标缓冲区清零,而不是把错误返回给调用方去当作“内存不存在”处理。两个直接后果:
- “地址找不到”这一症状表现为
0字节,而不是 helper 返回的-EFAULT。 - 读取发生在当前任务的页表上下文里,因为
copy_from_user_nofault()走的是当前任务的mm_struct,而不是任意某个任务的。
于是,当一个附着在任务 A 上下文里的 BPF 程序对“属于任务 B 地址空间”的用户态地址调用 bpf_probe_read_user() 时,要么发生访问故障(B 的页没有映射进 A 的地址空间——跨进程地址的常见情形)结果被清零;要么——在更罕见的情形里,同一个虚拟地址在两边都恰好被映射时——你读到的是 B 的值,而这并不是调用方想要的那个任务的值。helper 本身无法分辨这两种情况。
对大多数内核入口的 kprobe,该 helper 读的是正在执行被探针函数的任务的地址空间里的用户态地址——也就是说,是发起系统调用的那个进程。这正是实践者容易踩坑的地方:helper 读的是当前任务的内存,而不是你 map 里持有的某个 task_struct* 所对应任务的用户态地址空间。如果拥有该地址的任务并不是此刻运行探针的任务,读到的结果就不是你以为的。
目前并没有“从任务 A 上下文直接读任务 B 用户内存”的 helper,因为内核不想把一个不受信任的 BPF 程序变成任意跨任务内存访问的入口。两条受支持的做法:
路径一:程序运行在 B 上时读 B 的用户数据。
用 bpf_get_current_task_btf() 拿当前 task_struct(在任务上下文的 kprobe / uprobe / fentry 里有用),或者用 bpf_task_storage_get() 按 task_struct 作为 key 去读B 自己存进 BPF_MAP_TYPE_TASK_STORAGE 的每任务状态。手册页对该 helper 的说明:
bpf_task_storage_get(struct bpf_map *map, struct task_struct *task, void *value, u64 flags)—— “从task获取一个bpf_local_storage。从逻辑上可以理解为以task为 key 从一个 map 里取值……该 map 必须是BPF_MAP_TYPE_TASK_STORAGE。”
这正是读任务局部数据的正解:把程序附着到在 B 上触发的钩子(B 的用户库上的 uprobe、B 的入口上的 fentry),B 的 BPF task storage 里存着 B 自己的每任务状态。如果 B 需要看到自己的用户态内存,那应该是 B 自己的上下文在读,而不是外来上下文伸进来。
路径二:以任意 task_struct 为 key 的 BPF local storage。
BPF_MAP_TYPE_TASK_STORAGE 加 bpf_task_storage_get/bpf_task_storage_put 是把任意用户态数据挂到任务上、再让任意任务(包括稍后读它的那个)取回的规范化手段。这个 map 以 task_struct 为 key,所以打在 B 的某个内核入口上的 kprobe 可以 bpf_task_storage_get(map, target_task, …) 去读 B 的 BPF 局部数据,而稍后在 B 内部触发的 uprobe 也可以读回。这就是内核把“任务维度的 BPF 状态”变成一等公民的方式,无需新增 helper:任务自己在自己的上下文里写入,其他上下文通过 task key 读取。
对于任意的跨任务用户态指针解引用(即“我想在任务 A 的探针里跟随一个由任务 B 拥有的指针”),当前内核没有专门的 BPF helper。用户态指针不会被自动替你解引用:bpf_probe_read_user 是错的工具,因为它解引用的是当前任务的页表。实际解法是改变程序运行的时机:附着到在任务 B 内部触发的钩子(B 的 sys_enter_* 上的 kprobe、B 的 libc / 用户库上的 uprobe、或 B 调用的内核入口上的 fentry),再在那里读。
如何判断自己撞上了哪种情况
最小化的排查代码:
struct task_struct *t = (struct task_struct *)bpf_get_current_task_btf();
long r = bpf_probe_read_user(&out, sizeof(out), ptr_from_B);
if (r < 0) /* -EFAULT:当前任务里该地址不可读 */
/* 回退:该地址大概率属于一个不是当前任务的任务 */两条检查可以区分两种失败模式:
- 如果目标地址已知属于任务 B、而程序上下文不是 B,那么对于任何跨进程地址,该读取都会返回
-EFAULT(被映射为清零),改目标地址也救不回来——问题出在上下文。 - 如果目标地址是 B 的、而 B 其实正是当前任务(例如打在 B 自己的
sys_openat上的 kprobe),那普通的bpf_probe_read_user()就是好的——所以在怀疑 helper 之前先核对task->pid == target_pid。
对任务维度的数据,优先用 bpf_task_storage_get 而不是裸指针读取;它是不依赖页表巧合的内核准机制。
决定性的局限
一旦目标是另一个任务的用户态内存,bpf_probe_read_user() 就是错的工具,因为这个 helper 的语义是“在当前任务的地址空间里读,出错就静默返回全零”。两条受支持的逃生路线:把程序附着到在目标任务上触发的钩子;对任务维度的状态用 BPF_MAP_TYPE_TASK_STORAGE 加 bpf_task_storage_get/bpf_task_storage_put。当前 BPF helper 集合里没有“从一个与目标任务不匹配的 BPF 上下文读取任意其他任务用户内存”的 helper。
参考资料
- bpf-helpers(7) — Linux 手册页,
bpf_probe_read_user、bpf_probe_read_kernel、bpf_task_storage_get、bpf_get_current_task_btf - Linux 内核源码:
kernel/trace/bpf_trace.c - Linux 内核源码:
include/uapi/linux/bpf.h - Linux 内核文档:Program Types and ELF Sections
当日社区讨论
本监控窗口不具备技术性。过去 24 小时内,两个选择加入的 CNCF / Cilium Slack 归档只返回会议事务——一条“stabilization effort”例行会议通知(周一 / 周三 / 周五,美西时间)、一个带访问链接的 Zoom 会议、来自一位共同演讲者的两句致谢、一个 KubeCon 场次链接。没有任何 eBPF 问题、症状或设计争议。两个白名单中的 Discord 工作区(eunomia-bpf、sched-ext)本次运行没有可见的浏览器会话,公开邮件列表与论坛归档也未审阅。这些来源按“不可用覆盖”如实记录,而不是“安静”。由于可读归档没有产出问题,本篇问答以公开一手资料为依据,而非某条社区消息:这是一个反复出现的实践者问题——一个在任务 A 上触发的 BPF 探针无法安全地用 bpf_probe_read_user() 读取任务 B 的用户态指针,加载时要么失败、要么静默返回全零,而不是给出一个可读的错误——已对照 bpf-helpers(7) 手册页、上游 bpf_trace.c 实现以及 include/uapi/linux/bpf.h 做了核对。此处不复制任何私有文本、身份、频道或链接。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
为什么 BPF 程序不能睡眠或阻塞,在不可睡眠上下文里应该用什么替代?
简短回答: 一个 BPF 程序只有在两个条件同时成立时才可以睡眠:它是以可睡眠方式加载的(BPFFSLEEPABLE,附着到内核上下文允许睡眠的钩子上),并且当前执行不在任何临界区内。验证器会静态地同时检查这两点。如果一个可能睡眠的 helper 被从不可睡眠的程序里调用——或者从 bpfspinlock、bpfrcureadlock、禁止抢占的区域、禁止中断的区域内部调用——加载就会被拒绝,报错为 sleepable helper
下一篇 / 下一页
为什么给 Node.js 服务启用 eBPF 追踪后,热路径的 p99 会翻三倍,而 eBPF 探针本身却很廉价?
简短回答: 开销几乎全部来自注入到进程内的 JavaScript 代理,而不是 eBPF 探针本身。OBI 这类基于 eBPF 的自动插桩工具,对 Node.js 的"零代码"支持并不是纯 eBPF 实现的:它通过 Node 检查器协议(SIGUSR1 + Runtime.evaluate)把一个 JS 代理注入到应用里,而这个代理的 asynchooks before 钩子会在每一个 JS 回调之前触发,用来刷新活动请求的
- 最后更新
- 2026年9月16日
- 首次发布
- 2026年9月16日
- 贡献者
- Yunwei 123
这个页面有帮助吗?