土法炼钢 · 系统与基础设施

【Tetragon / eBPF】Sensor 与 Hook:进程传感器、kprobe、tracepoint、uprobe、LSM 与 fentry

文章导航

分类入口
kubernetessecurity
标签入口
#tetragon#ebpf#kprobe#tracepoint#uprobe#lsm#fentry#tracingpolicy#v1.7.0

目录

第 2 篇 钉了 exec_idexecve_map;本篇钉 Sensor/Hook 轴。读者常见误区有两个:一是以为写一条 TracingPolicy 就覆盖了进程生命周期;二是把 kprobe 系统调用入口当成「已经看到内核最终用的那个对象」。真正要钉死的是:__base__ 传感器负责 exec/clone/exit 与 map;TracingPolicy 按 hook 类型另挂程序;选错类型会表现为无事件、错参数或踩上 TOCTOU,而不是 YAML 没 apply。

本篇在系列中的位置

篇目 核心内容
第 2 篇 · 进程模型 exec_idexecve_map
第 3 篇 · Sensor 与 Hook 内置进程传感器;kprobe / tracepoint / uprobe / LSM / fentry
第 4 篇 · 事件导出路径 JSON / gRPC 分流
系列目录 全部篇目

版本锚定:Tetragon v1.7.0docs/.../concepts/tracing-policy/hooks.mdbpf/process/bpf_generic_*.cpkg/sensors/base/;CRD tracing_policy_types.go)。hooks.md 正文以 kprobe / tracepoint / uprobe / USDT / LSM 为主;spec.fentries 以 CRD 与 bpf_generic_fentry.c 为准,不要用 live 文档补一篇本 tag 没有的 fentry 教程。Verifier 算法外链 ebpf/,本篇不重写。无真实节点则不粘贴伪造 hook 事件。


一、base / process sensor

进程生命周期不是 TracingPolicy 的默认附件。v1.7.0 用名为 __base__ 的 sensor 加载一组固定程序(pkg/sensors/handler.goBaseSensorNamepkg/sensors/base/base.goGetInitialSensor)。Linux 默认程序列表在 base_linux.goExitForkExecveExecveBprmCommitExecveMapUpdate

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/sensors/base/base.go
// function: package-level Builders (excerpt)
Execve = program.Builder(
    config.ExecObj(),
    "sched/sched_process_exec",
    "tracepoint/sys_execve",
    "event_execve",
    "execve",
).SetPolicy(sensors.BaseSensorName)

Fork = program.Builder(
    config.ForkObj(),
    "wake_up_new_task",
    "kprobe/wake_up_new_task",
    "kprobe_pid_clear",
    "kprobe",
).SetPolicy(sensors.BaseSensorName)
程序 用户态 Builder 锚点 BPF 入口 职责
Execve attach sched/sched_process_exec,类型 tracepoint/sys_execveevent_execve bpf/process/bpf_execve_event.c EVENT_EXECVE,更新 execve_map,输出执行事件
Fork kprobe/wake_up_new_task bpf/process/bpf_fork.c 每线程组一次 clone;父不在 map 则早退
Exit 优先 kprobe/acct_process,否则 disassociate_ctty bpf/process/bpf_exit.c 线程组最后任务上发送 exit
ExecveBprmCommit kprobe/security_bprm_committing_creds bpf_execve_bprm_commit_creds.o 在凭据提交点记录 secureexec / inode 信息,供 execve 拼接
ExecveMapUpdate socket 程序 execve_map_update 大程序内核上更新 match-binaries 集合

共享 map 包括 execve_maptg_conf_map、统计 map,以及内核 ≥ 某代对象文件时的 tg_rb_events ring(base_linux.go 把 ringbuf 门闩放在 v5.11 对象集,并可用 perf ring 回退)。用户态 pkg/sensors/exec/exec_linux.goMSG_OP_EXECVE / EXIT / CLONE 登记到 observer。

TracingPolicy 通过 registeredPolicyHandlers 另造 sensor 集合,加载进同一 collection map,但 不会替代 __base____base__ 在卸载时最后销毁。失败表象:策略事件偶发出现,但没有任何 process_exec——先查 base sensor 是否加载、BTF 是否足够跑 execve 对象,而不是先改 kprobe YAML。

v1.7.0 Tracing Policy 索引页的导语只写「kprobes、tracepoints、uprobes」。同 tag 的 CRD 还包含 lsmhooksusdtsfentries以 CRD 为准;导语不是能力闭包。


二、kprobe / kretprobe

Kprobe 在任意内核函数入口执行 BPF。Tetragon 用 spec.kprobes 描述,对应 bpf/process/bpf_generic_kprobe.c。入口 generic_kprobe_event 先跑进程过滤再 setup / 读参 / 选择器 / 动作 / 输出。文件头注释给出伪代码顺序:pid → namespace → capability → 拷贝参数 → selector → ring。老内核靠 tail call 把指令数压到 4k 限制以下。

// project: cilium/tetragon, tag: v1.7.0
// path: bpf/process/bpf_generic_kprobe.c
// function: generic_kprobe_event
__attribute__((section((MAIN)), used)) int
generic_kprobe_event(struct pt_regs *ctx)
{
    return generic_start_process_filter(ctx, (struct bpf_map_def *)&kprobe_calls);
}

kprobe_callsBPF_MAP_TYPE_PROG_ARRAYTAIL_CALL_SETUP / PROCESS / FILTER / ARGS / ACTIONS / SEND,小程序内核再拆 PROCESS_2 / ARGS_2。排障「策略已加载但 CPU 高、无事件」时,先问过滤是否在 generic_process_filterPFILTER_REJECT,而不是先怀疑 ring 满——后者是第 11 篇。

syscall: true 告诉 Tetragon 按系统调用 ABI 读参,而不是普通 C 函数 ABI。文档 Caution:写可移植策略时用无架构前缀的 sys_write,由 agent 补 __x64_ / __arm64_输出事件里仍可能带前缀/proc/kallsyms 用来核对本机真实符号;符号随内核版本变化,这是 kprobe 相对 tracepoint 的主要代价。

返回值与「入口处尚未填满的缓冲区」走 kretprobe:return: truereturnArgchar_buf 上的 returnCopy 表示到返回时再读。bpf_generic_retkprobe.c 是对应对象。sizeArgIndex 文档写明当前实现按「第 n 个参数」计数(第三个参数写 3 而不是 index 2),属于已知不一致。

同一文件里还有 generic_kprobe_override:从 override_tasks 取出错误码并 override_return。这是强制执行轴的挂载点,本篇只登记「Override 长在 kprobe 上」;保证与 CONFIG_BPF_KPROBE_OVERRIDE 见第 10 篇。v1.7.0 genericfentry.go 写明 fentry 尚不支持 enforcement

hooks.md 开篇 Warning:hook 系统调用且参数是用户态指针时存在 TOCTOU——用户态可在 hook 之后、内核消费之前改内存。要看内核已拷贝的对象,应改挂稍后的函数或 LSM security_*。这是 Garfinkel(NDSS 2003)在工程文档里的直接对应,不是产品修辞。

文档示例(配置片段,非本机加载结果):

spec:
  kprobes:
  - call: "fd_install"
    syscall: false
    args:
    - index: 0
      type: "int"
      label: "FD"
    - index: 1
      type: "file"

argstype 告诉 BPF 如何读参。char_buf / char_iovec 可加 returnCopy(到 kretprobe 再读)与 sizeArgIndex。hooks.md Caution:sizeArgIndex 当前按「第 n 个参数」计数(第三个参数写 3 而不是 index 2)。默认 char_buf 最多 4096 字节;maxData: true 在文档所述内核 ≥5.4 上可到 327360 字节,且不能returnCopy 同用。resolve 从内核结构体解字段:kprobe ≥5.4,LSM ≥5.7,uprobe 不支持。

spec.lists 可以把一组符号折叠进 call: "list:NAME"type: syscalls 的列表不能与普通函数混用;generated_syscalls / generated_ftrace 会在加载时展开。列表共享同一套 args 与 selector——签名不同的 open / openat 不能靠 list 共用 matchArgs index,文档改走 selectorsMacros

选错 kprobe 的失败表象:符号带错前缀导致加载失败;hook sys_open 却去读内核 struct file(那是 fd_install / LSM file_open 的世界);returnCopy 没开导致 read 缓冲区为空;list 里混了 syscall 与非 syscall。


三、tracepoint / rawtp

Tracepoint 是内核静态埋点,ABI 比 kprobe 稳定。策略写 spec.tracepoints,指定 subsystemevent。可用点在 /sys/kernel/tracing/events;参数布局读该事件的 format 文件。文档示例挂 raw_syscalls/sys_enter 并用 index: 4 取 syscall ID。BPF 对象为 bpf_generic_tracepoint.c

Raw tracepointraw: true)仍 attach 同一类事件,但参数是 TRACE_EVENT 的原始原型,而不是 perf 格式化后的字段。文档口径:不经过中间 perf 层,应比标准 tracepoint 更快。示例:sched/sched_process_execraw: true 后,index: 2 类型 linux_binprm 可直接拿出路径。Resolve 可从 task_struct 解字段(如 pid)。对象文件 bpf_generic_rawtp.c

__base__ 的 exec 传感器自己就挂在 sched/sched_process_exec。再写一条相同 tracepoint 的 TracingPolicy 会多一份事件,不是「让 base 更准」。syscall 跟踪优先 raw_syscalls / syscalls 子系统,而不是为每个 sys_* 发明不稳定 kprobe 名——除非你需要 kprobe 才有的 Override。

失败表象:format 里的 index 与 raw 原型 index 混用;把 rawtp 的结构体参数当成已经格式化的字符串。


四、uprobe / USDT

Uprobe 挂用户态函数:spec.uprobes 提供二进制/库 pathsymbols。符号随二进制版本变化,跨发行版不可移植。文档用 nm -D /bin/bash 确认 readline 在 text 段。选择器与 kprobe 同一套(matchArgsmatchReturnArgsmatchBinaries 等)。返回值要 return: true。对象:bpf_generic_uprobe.cbpf_generic_retuprobe.c

hooks.md 写明:uprobe 不支持内核结构体 resolve(那是 kprobe/LSM 的 BTF 遍历)。

USDT(User Statically-Defined Tracing)读 ELF stapsdt note。spec.usdtspath + provider + name。每个 probe 最多 5 个参数(Tetragon 限制)。tetra tracingpolicy generate usdts --binary ... 可从二进制生成策略骨架。对象:bpf_generic_usdt.c

失败表象:path 指向容器内路径而 agent 看到的是主机 mount;符号只在静态二进制里、nm -D 看不到;USDT semaphore 未启用导致 probe 静默。无事件时先用 readelf -n 确认 note,再查 TracingPolicy 是否加载(第 5–6 篇)。


五、LSM 与 fentry(v1.7.0)

LSM BPF

spec.lsmhooks 挂 LSM 钩子,文档指向 security/security.c 中的 hook 名。前置条件(hooks.md):

  1. 内核 CONFIG_BPF_LSM=y
  2. /sys/kernel/security/lsm 列表包含 bpf;否则需把 bpf 加入 LSM 启动参数后重启。

示例策略 hook file_open,参数类型 file,再 matchBinaries + matchArgs 限制 /usr/bin/cat 打开 /etc/passwd。对象:bpf_generic_lsm_core.c

LSM 相对 syscall kprobe 的机制收益:参数已是内核对象,Warning 中的用户态指针 TOCTOU 被推到更早的拷贝完成之后。这不是「LSM 消灭竞态」,而是把检查点移到内核驻留状态。属性 resolve 对 LSM 要求内核 ≥ 5.7(hooks.md Caution);kprobe 的 resolve 从 5.4 起。

Tetragon 在 LSM hook 上跑 eBPF 程序,不是自己实现一套 LSM 框架。学术前身是 Wright et al.(USENIX Security 2002)的 LSM 接口;本篇只把 Tetragon 定位为该接口上的 BPF 客户。展开见第 5–8 篇的策略加载与身份过滤。

fentry

CRD TracingPolicySpec.Fentries 类型为 []KProbeSpec,JSON 键 fentries

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/k8s/apis/cilium.io/v1alpha1/tracing_policy_types.go
// struct: TracingPolicySpec
    // A list of fentry specs.
    Fentries []KProbeSpec `json:"fentries,omitempty"`

加载路径:pkg/sensors/tracing/genericfentry.go 注册 probe 类型 generic_fentry,并调用与 kprobe 共享的 createGenericKprobeSensor。若选择器含 enforcement 动作,加载在用户态失败:

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/sensors/tracing/genericfentry.go
// function: createGenericFentrySensor
    for _, fentry := range spec.Fentries {
        if selectors.HasEnforcementAction(fentry.Selectors) {
            return nil, errors.New("enforcement is not supported for fentry yet")
        }
    }

BPF 入口:

// project: cilium/tetragon, tag: v1.7.0
// path: bpf/process/bpf_generic_fentry.c
// function: generic_fentry_event
__attribute__((section((SECTION_ENTRY)), used)) int
generic_fentry_event(struct pt_regs *ctx)
{
    return generic_start_process_filter(ctx, (struct bpf_map_def *)&fentry_calls);
}

section 为 fentry/generic_fentry。tail call 链与 generic kprobe 同构。输出 opcode 仍是 MSG_OP_GENERIC_KPROBE;用户态信封以 observer 对该 opcode 的映射为准,不要假设存在独立的 PROCESS_FENTRY 枚举(v1.7.0 events.protoEventType 无此值)。

同目录存在 bpf_generic_fexit.c,作为 fentry 族返回路径对象;CRD 没有单独的 fexits 字段。hooks.md @ v1.7.0 没有 fentry 专节。能力陈述以 CRD + 上述源码为准,不以 live 站点的后补教程为准。fentry 依赖 BTF 与 tracing 类程序;加载失败时表象与「写了 kprobe 但内核没有该符号」类似,应查 agent 日志与 BTF,而不是先改 matchArgs。Trampoline / 验证器细节见 ebpf/


六、hook 类型对照,以及与 ebpf/ 的分工

Hook 类型 TracingPolicy 字段 能观察什么 内核 / 配置依赖 选错时的典型失败
内置 process sensor 无(__base__ exec / clone / exit、维护 execve_map BTF/CO-RE 对象;exit 符号 acct_processdisassociate_ctty process_exec;fork 父缺失
kprobe kprobes 任意内核函数入口参数 符号存在;syscall 需正确 ABI 无事件;参数全是错的
kretprobe kprobes + return 返回值、返回时才填充的缓冲 与 kprobe 相同 空 buffer;sizeArgIndex 数错
tracepoint tracepoints 稳定跟踪点的格式化字段 tracefs 上存在该 event format index 错
raw tracepoint tracepoints + raw: true TP_PROTO 原始参数 同上;文档称绕过 perf 层 与 format index 混用
uprobe uprobes 用户态函数参数/返回值 二进制 path+符号匹配 路径/符号版本不对
USDT usdts 应用静态探针,最多 5 参数 ELF stapsdt 无 note 或 semaphore
LSM lsmhooks 内核安全对象(file/path/cred) CONFIG_BPF_LSM 且 lsm 列表含 bpf 加载被拒;无 bpf LSM
fentry fentries 带 BTF 的函数入口(策略形状同 kprobe spec) BTF、tracing;无 enforcement 加载失败;误以为能 Override
flowchart TD
  base["__base__ exec fork exit"] --> map["execve_map"]
  tp["TracingPolicy"] --> kprobe["kprobe / kretprobe"]
  tp --> trace["tracepoint / rawtp"]
  tp --> uprobe["uprobe / USDT"]
  tp --> lsm["LSM hooks"]
  tp --> fentry["fentries"]
  map --> kprobe
  map --> trace
  map --> uprobe
  map --> lsm
  map --> fentry

图:动态 hook 读取 __base__ 维护的进程状态;它们不负责把进程第一次写进 map。

ebpf/ 的分工:该系列写 verifier 抽象解释、JIT、Map 实现、fentry trampoline、BTF/CO-RE。本篇只写 Tetragon 把哪些程序类型接到 TracingPolicy 字段上,以及选错时的产品失败表象。不要在本篇寻找「为什么这段 BPF 被 verifier 拒绝」的算法答案。

谱系:McCanne & Jacobson(USENIX Winter 1993)把过滤放进内核 VM;eBPF 把 VM 接到 kprobe/tracepoint/LSM。Garfinkel(NDSS 2003)给出 syscall 拦截的 TOCTOU 与覆盖不全;hooks.md 的 Warning 是该争论的文档化版本。Höiland-Jørgensen et al.(CoNEXT 2018)的 XDP 早路径不解释 file_openwake_up_new_task——那是数据面对照。

争论:CR 便利 vs 低层 hook 误配。YAML 让策略可声明,但 call 字符串仍是内核符号。无事件时,社区容易把它解释成「eBPF 不可靠」;v1.7.0 源码把更多失败推到「符号/ABI/LSM 列表/BTF」这些可核对条件上。另一侧是用户态规则引擎:可移植、不绑符号,但把 TOCTOU 窗口留在用户态(第 14 篇对照 Falco)。

开放问题:标签与容器选择器如何限制哪些 cgroup 真正 attach(第 8 篇);fentry 上的 enforcement 何时进入稳定语义(v1.7.0 明确未支持)。


七、小结

  1. __base__ 维护进程生命周期与 execve_map;TracingPolicy 只增加动态 hook。
  2. kprobe 能力最宽、可移植性最差,且 syscall 指针参数有 TOCTOU。
  3. tracepoint / rawtp 用稳定埋点换原始或格式化参数;raw 更快、index 语义不同。
  4. uprobe / USDT 观察用户态;USDT 有 5 参数上限。
  5. LSM 需要 CONFIG_BPF_LSMbpf 在 lsm 列表;fentry 在 v1.7.0 经 spec.fentries 提供,且不能做 enforcement。下一轴是导出:hook 产生的事件如何在 JSON 与 gRPC 分成两套可见集合。

八、参考资料

规范 / 官方文档(A)

源码(A)

核心论文

实验 / 工具

站内对照


上一篇:进程模型 · 系列目录 · 下一篇:事件导出路径

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .