第 1
篇 把排障收束到五轴;本篇钉
进程血缘轴。读者常见误区有两个:一是把
Tetragon 的进程树理解成「对 /proc
做了更快的采样」;二是以为只要装上
agent,血缘就是完整的。真正要钉死的是:exec_id
解决 PID 复用,不解决 map 未命中;内核
execve_map 是执行期权威状态,JSON
里的父子来自用户态 cache 的富化;fork 时若父不在
map,子进程不会被写入 map。
本篇在系列中的位置
篇目 核心内容 第 1 篇 · 运行时安全全景 五轴坐标系 第 2 篇 · 进程模型 exec_id、血缘、in_init_tree、procfs 与 fork 失败路径第 3 篇 · Sensor 与 Hook 内置进程传感器与动态 hook 系列目录 全部篇目
版本锚定:Tetragon v1.7.0(源码 tag v1.7.0,重点
pkg/process/、bpf/lib/process.h、bpf/process/bpf_fork.c)。机制叙述对齐官方 Events 文档与tetragon.proto的Process注释。无真实节点则不粘贴伪造process_exec输出;下文 JSON 片段来自 v1.7.0 文档示例,不是本机采集。
一、process_exec
/ process_exit 字段
默认安装会产生 process_exec 与
process_exit 两类基础事件。v1.7.0 Events
文档把执行事件的主键钉在
process_exec.process.exec_id,并把 Kubernetes
元数据放在
process_exec.process.pod。二进制与参数是
process.binary 与
process.arguments。所有事件类型都带
node_name 与 time。
api/v1/tetragon/tetragon.proto 里
Process 与两个信封消息的职责如下(字段以 proto
为准,不是「文档示例里碰巧出现的键」):
| 字段 / 消息 | 作用 | 注意 |
|---|---|---|
Process.exec_id |
跨节点、跨时间标识一次执行 | 不是 PID |
Process.pid |
主机 PID 命名空间里的 TGID | 容器内 PID 在 pod.container.pid |
Process.tid |
线程 ID;线程组首领上 tid 等于 pid | 缓存按 TGID,其他事件再填精确 tid |
Process.parent_exec_id |
父进程的 exec_id |
父不在 cache 时可能走恢复路径 |
Process.pod |
namespace / name / container / labels / workload | 主机进程没有该字段 |
Process.flags |
调试用位的空格分隔字符串 | proto 明确写不可当作可靠语义源 |
Process.in_init_tree |
是否为容器 PID 1 子树成员 | 主机进程为 false |
ProcessExec.process / parent /
ancestors |
执行事件的自身、直接父、更上层祖先 | 祖先需配置开启,见 filters 对 ancestors 的门闩 |
ProcessExit.process / parent /
signal / status |
退出原因 | 无信号处理时看 status |
下面这段是 v1.7.0
官方文档示例(经删减,只保留本篇用到的键),用来对照字段位置,不是本机
tetra 输出:
{
"process_exec": {
"process": {
"exec_id": "Z2tlLWpvaG4tNjMyLWRlZmF1bHQtcG9vbC03MDQxY2FjMC05czk1OjEzNTQ4Njc0MzIxMzczOjUyNjk5",
"pid": 52699,
"binary": "/usr/bin/curl",
"arguments": "https://ebpf.io/applications/#tetragon",
"flags": "execve rootcwd",
"parent_exec_id": "Z2tlLWpvaG4tNjMyLWRlZmF1bHQtcG9vbC03MDQxY2FjMC05czk1OjEzNTQ4NjcwODgzMjk5OjUyNjk5",
"pod": {
"namespace": "default",
"name": "xwing",
"container": { "name": "spaceship", "pid": 49 }
}
},
"parent": {
"exec_id": "Z2tlLWpvaG4tNjMyLWRlZmF1bHQtcG9vbC03MDQxY2FjMC05czk1OjEzNTQ4NjcwODgzMjk5OjUyNjk5",
"binary": "/bin/bash",
"flags": "execve rootcwd clone"
}
},
"node_name": "gke-john-632-default-pool-7041cac0-9s95"
}flags 里的 execve
表示这次身份来自 execve 路径;clone
表示该进程在 exec 之前经历过 clone。rootcwd
对应内核 EVENT_ROOT_CWD:CWD 是
/,不占事件缓冲区。完整位到字符串的映射在
pkg/reader/exec/exec.go 的
DecodeCommonFlags。proto 注释还列出
procFS、miss、truncArgs
等;把它们当产品功能开关会误判。
能力集、命名空间、完整 credentials 默认关闭,分别由
--enable-process-cred、--enable-process-ns
打开。用户名解析还要求 --username-metadata=unix
且进程在主机用户命名空间——并且文档写明这是用户态查
/etc/passwd,hook
执行到解析之间映射可能已变。
二、exec_id
如何生成
PID 会回绕。Tetragon 的稳定键不是「节点上的
PID」,而是一次执行的 exec_id。Linux
上的生成函数只有一行有效逻辑:
// project: cilium/tetragon, tag: v1.7.0
// path: pkg/process/process_id_linux.go
// function: GetProcessID
func GetProcessID(pid uint32, ktime uint64) string {
return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%d:%d", node.GetNodeNameForExport(), ktime, pid))
}三元组含义:
| 分量 | 来源 | 消除什么 |
|---|---|---|
| 导出用节点名 | GetNodeNameForExport():先
HUBBLE_NODE_NAME,再
NODE_NAME,再内核 hostname |
跨节点 PID 碰撞 |
ktime |
该次执行在内核侧记录的时间戳 | 同一节点上 PID 复用 |
pid |
TGID | 与内核 execve_map 的键对齐 |
GetExecID / GetExecIDFromKey
分别从 MsgProcess 与 MsgExecveKey
调到同一函数。父进程的 parent_exec_id 用父的
(pid, ktime) 再算一遍。若父 PID 为
0,initProcessInternalExec 会用
GetProcessID(0, 1)
填一个占位父——这是「没有可用父键」时的工程填缝,不是 PID 0
真的 exec 过。
proto 注释写 exec_id「uniquely identifies
the process over time across all the nodes in the
cluster」。该唯一性依赖导出节点名在集群内不碰撞,以及
ktime 在 PID 复用周期内足够区分两次执行。节点改名或
HUBBLE_NODE_NAME 与 Kubernetes
节点名不一致时,导出身份与 API server 里的 node
对不齐——pkg/reader/node/node.go
为此把「导出用名」和「watch Pod 用名」分成两个函数。
exec_id 消除的是 PID
作为跨时间主键;它不消除「内核根本没为这次
clone/exec 建过 map 条目」。下一节把这两层状态拆开。
三、execve_map
与用户态 cache
flowchart LR
tp["sched_process_exec / wake_up_new_task"] --> map["execve_map TGID key"]
procfs["procfs snapshot at start"] --> map
map --> ring["Ring or perf buffer"]
ring --> cache["userspace process cache LRU"]
cache --> json["JSON process.exec_id"]
图:内核 map 先记下 (pid, ktime)
与父键;用户态 cache 用 exec_id 做 LRU
键,再序列化成 JSON。
内核:execve_map
bpf/lib/process.h 把 map 值定义为按
TGID 跟踪的
execve_map_value:自身 key(pid +
ktime)、父
pkey、flags、nspid、命名空间、capability、binary。线程
ID 不作为 map 主键;注释写明 TID
随具体事件附带。默认最大条目在
pkg/sensors/base/base.go 为
execveMapMaxEntries = 32768,可被配置项覆盖。本篇不把条目数换算成内存字节——换算依赖结构体填充与内核分配器,未在本仓库实测。
execve 路径在
bpf/process/bpf_execve_event.c:程序挂在
tracepoint/sys_execve(用户态加载时 attach
sched/sched_process_exec)。event_execve
填
EVENT_EXECVE,读路径、参数、CWD;execve_send
用 execve_map_get_noinit 更新当前 TGID 的
key / pkey / binary,并
event_output_metric 送出。clone
路径见第五节。
用户态:process cache
pkg/process/cache.go 用
hashicorp/golang-lru 按
Process.ExecId 字符串缓存
ProcessInternal。add 只应从 clone
或 execve 事件调用。引用计数为 0 后进入分色
GC(inUse → deletePending →
deleteReady →
deleted);过早删除记
tetragon_process_cache_early_deletions_total(v1.7.0
引入)。LRU 满时驱逐仍 inUse 的条目会给父做
parent--,避免父引用泄漏。
AddExecEvent 构造 cache 条目:若
CleanupProcess.Ktime == 0 或带 clone
标志,父键用 Linux 提供的 Parent;否则用
CleanupProcess。这是「execve_map
里找不到旧条目」时的退路,不是第二条完整进程树。
JSON 导出的 process / parent 是
cache 里 protobuf 的克隆,再叠加 Pod
信息(GetPodInfo)。map 有、cache
已驱逐时,后续 hook 事件仍可能从 BPF 带出
pid/ktime,但用户态会表现为 miss
或残缺父信息。proto 对 miss 的口径是:buffer
里找不到父,Tetragon
尽量从内核数据结构恢复,参数不可用,并建议当作应报给开发者的错误标志。
两层状态不要合并成一句「Tetragon 维护完整进程树」:内核 map 服务 hook 与选择器;用户态 cache 服务导出与祖先展开。任何一层未命中都是血缘轴故障,不是「eBPF 没装上」。
四、in_init_tree
与注入
容器里「PID 1 的子孙」与「后来被 nsenter /
kubectl exec / docker exec
塞进来的进程」在主机 PID 命名空间里看起来都像同一个
Pod。Tetragon 用 in_init_tree 区分它们。
内核
set_in_init_tree(bpf/process/bpf_process_event.h)的规则是:
- 父条目已带
EVENT_IN_INIT_TREE,则子继承。 - 否则若当前
nspid == 1(容器 PID 命名空间内的 1 号进程),则自己置位。
// project: cilium/tetragon, tag: v1.7.0
// path: bpf/process/bpf_process_event.h
// function: set_in_init_tree
FUNC_INLINE void
set_in_init_tree(struct execve_map_value *curr, struct execve_map_value *parent)
{
if (parent && parent->flags & EVENT_IN_INIT_TREE) {
curr->flags |= EVENT_IN_INIT_TREE;
return;
}
if (curr->nspid == 1) {
curr->flags |= EVENT_IN_INIT_TREE;
}
}execve 的 execve_send 在更新 map 后调用
set_in_init_tree(curr, NULL),只靠 当前
nspid=1 置位,再把标志抄进事件。clone 路径传入
parent,因此容器 init 的子进程靠继承,而不是每个子进程都变成
nspid=1。用户态 initProcessInternalExec /
initProcessInternalClone 把该位变成 protobuf
BoolValue。
导出过滤里的
in_init_tree(events.proto 的
Filter.in_init_tree)文档口径与此一致:用来监视经
docker exec、kubectl exec 等机制注入的进程。主机进程的 nspid
逻辑不会把它标成容器 init 子树,值为 false。
失败表象:
| 现象 | 优先检查 |
|---|---|
业务入口进程 in_init_tree=false |
容器 PID 1 是否在 agent 启动前已存在且 procfs 回填未标上(见第五节);或该进程其实是注入会话 |
kubectl exec 的 shell
in_init_tree=true |
该 shell 是否由容器内已有 init 子孙 spawn,而不是从主机 nsenter |
过滤 in_init_tree=true 仍看到 exec
会话 |
导出过滤与 gRPC 是否同一 sink(第 4 篇) |
in_init_tree
消除的是「凡是在这个容器里跑的都算业务树」;它不消除「PID
1 自己没进 map」时整棵树无法继承。
五、procfs 回填与失败路径
启动快照:procFS
agent
起来之前,节点上已经有进程。pkg/sensors/exec/procevents/proc_reader_linux.go
的 listRunningProcs 扫配置的 procfs
目录,给每个 TGID 填
flags: api.EventProcFS | api.EventNeedsCWD,再
writeExecveMap 写入内核
map。DecodeCommonFlags 把该位渲染成字符串
procFS。proto
要求一次正确格式化的事件应带 execve 或
procFS 之一。
procToKeyValue 用与内核相同的规则回填
EVENT_IN_INIT_TREE:nspid == 1
或父已在本次扫描构造的 inInitTree
集合中。这修复了「容器先于 Tetragon 启动则 in_init_tree
全错」的窗口;它仍然是扫描时刻的快照。扫描期间退出的短命进程不会出现在
map 里——Garfinkel(NDSS 2003)讨论过依赖 /proc
重建进程状态的竞态,这里是工程对应物。
StartTime 对 procfs 条目的处理与 live exec
不同:initProcessInternalExec 在
(process.Flags&api.EventProcFS) == 0 时才把
ktime 转成可导出时间戳选项。不要用 procfs
回填事件的绝对时间去对齐审计时钟而不看
flags。
fork
失败路径:父不在 execve_map
clone 不是「用户态再读一遍
/proc」。bpf/process/bpf_fork.c
把程序挂在
kprobe/wake_up_new_task。关键控制流:
// project: cilium/tetragon, tag: v1.7.0
// path: bpf/process/bpf_fork.c
// function: event_wake_up_new_task
parent = __event_find_parent(task);
if (!parent)
return 0;
curr = execve_map_get(tgid);
if (!curr)
return 0;
if (curr->key.ktime != 0)
return 0;注释写明:找不到父就不创建消息,也不调用会插入新条目的
execve_map_get。
父缺失时子进程不会进入
execve_map。每个线程组只生成一次
MSG_OP_CLONE(靠
curr->key.ktime != 0 去重)。用户态
AddCloneEvent 若 cache 里没有父
exec_id,同样直接返回错误,不造孤立子条目。
这就是本系列禁止复述「零缺口血缘」的源码原因。缺口至少包括:
- 父从未进 map(agent 启动窗口、map 满、更早一次 fork 已失败)。
- 父已从 map 删除(进程组已 exit),子仍在 fork。
- procfs 扫描与 live hook 之间的 TOCTTOU:扫描时父在,hook 时父已退出;或相反。
exit 侧 bpf/process/bpf_exit.c hook
acct_process(若符号不存在则退到
disassociate_ctty),只在线程组最后一个任务上发送,避免每个线程一次
exit 事件。map 删除与 cache process--
在这条路径上汇合;细节排障见第 13 篇。
开放问题(本篇只立靶):启动快照丢失祖先时,后续 live
事件的血缘是否永久残缺——取决于父是否永远无法被补进
map。Tetragon 不会在用户态「猜一个历史父进程」来填
exec_id。可观测入口是 cache
dump、execve_map dump(v1.7.0 bugtool)与
miss / early deletion 指标。
六、学术谱系、争论与开放问题
谱系:经典审计与主机 IDS 用 PID 加
/proc 快照描述进程。Garfinkel(NDSS
2003)指出系统调用拦截工具若在用户态补进程树,会撞上短命进程与
TOCTTOU。McCanne & Jacobson(USENIX Winter
1993)提供的是「把过滤放进内核虚拟机」的范式;Tetragon
把该范式用在执行跟踪——execve_map 是 BPF map
状态,不是 XDP 数据包环。Höiland-Jørgensen et al.(CoNEXT
2018)的 XDP 假设面向网卡早路径,与
wake_up_new_task 上的 clone
跟踪不是同一代价模型。
争论:内核进程树 vs procfs 快照
| 侧 | 主张 | 代价 |
|---|---|---|
仅 /proc |
实现简单,不依赖专用 map | 短命进程在两次采样之间消失;与拦截不同步 |
内核 execve_map |
exec/clone/exit 与选择器共用同一份状态 | 父未命中则子不入图;启动仍必须扫 procfs |
没有文献能宣布某一侧在所有节点生命周期下零窗口。可检验的工程命题是:在你的
churn 下,空洞是否可被 procFS /
miss / fork
早退归因。若团队要求「每个历史父进程都可还原」,需要在
agent 启动顺序与 map 容量上单独设计,而不是指望
exec_id 本身。
开放问题:
- fork
失败是否永久残缺:父稍后出现(例如用户态补偿)能否补边——v1.7.0
的
bpf_fork.c早退不写子条目。入口:本文件第五节;第 13 篇。 - 导出完备性:采样、节流、cache 驱逐之后,JSON 所见树是否仍够取证。入口:第 4、11 篇。
- 与 Cilium identity
联合归因:
exec_id与 numeric Identity 如何写进同一 runbook——第 16 篇。
七、小结
exec_id是base64(node:ktime:pid),消除 PID 复用,不消除 map 未命中。execve_map按 TGID 保存内核权威状态;process cache 按exec_id服务导出。in_init_tree标记容器 PID 1 子树,用来分开kubectl exec一类注入。procFS是启动回填标志,不是 live execve。- 父不在 map 则 fork 不入图——这是失败路径。下一轴是 hook:身份只有被进程传感器和 TracingPolicy 程序读到才成为事件。
八、参考资料
规范 / 官方文档(A)
- Tetragon v1.7.0 Events —
docs/content/en/docs/concepts/events.md@ v1.7.0(exec_id、process_exec文档示例、in_init_tree过滤说明)。 api/v1/tetragon/tetragon.proto@ v1.7.0(Process、ProcessExec、ProcessExit字段注释,含flags/in_init_tree)。
源码(A)
pkg/process/process_id_linux.goGetProcessID;pkg/reader/node/node.goGetNodeNameForExport@ v1.7.0pkg/process/process.go(initProcessInternalExec/AddExecEvent/AddCloneEvent);pkg/process/cache.go@ v1.7.0bpf/lib/process.h(execve_map_value、EVENT_PROCFS、EVENT_IN_INIT_TREE)@ v1.7.0bpf/process/bpf_execve_event.cevent_execve/execve_send;bpf/process/bpf_fork.cevent_wake_up_new_task;bpf/process/bpf_exit.c;bpf/process/bpf_process_event.hset_in_init_tree@ v1.7.0pkg/sensors/exec/procevents/proc_reader_linux.golistRunningProcs/procToKeyValue;pkg/reader/exec/exec.goDecodeCommonFlags@ v1.7.0
核心论文
- McCanne & Jacobson, The BSD Packet Filter, USENIX Winter 1993。
- Garfinkel, Traps and Pitfalls, NDSS 2003 ——
/proc重建窗口与拦截完整性。 - Höiland-Jørgensen et al., The eXpress Data Path, CoNEXT 2018 —— 数据面对照,非进程树主线。
实验 / 工具
- 本篇无节点命令输出。JSON 为官方文档示例。不引用站内未核实的「零缺口」或内存 MB 对照。
站内对照
→ 上一篇:运行时安全全景 · 系列目录 · 下一篇:Sensor 与 Hook
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Tetragon / eBPF】运行时安全全景:缺口、五轴坐标系与 16 篇路线
相对 Cilium 数据面、可观测对照与 Falco 口号,钉清 Tetragon v1.7.0 运行时安全内核缺口;定义进程血缘、Sensor/Hook、策略加载、过滤分层、强制执行五条坐标系,给出 16 篇阅读路线。
【Tetragon / eBPF】排障坐标系:无事件、策略不生效、导出缺口、误阻断与血缘空洞
按五轴映射 Tetragon v1.7.0 故障:无事件落在 Hook/BTF/加载,策略不生效落在 collectionKey 撞名与 policyfilter,JSON allow/deny 不等于 gRPC,Override 依赖 CONFIG_BPF_KPROBE_OVERRIDE,血缘空洞来自 execve_map 未命中与 procFS 回填。
Tetragon / eBPF 运行时安全:从进程血缘到 TracingPolicy
补齐站内 Cilium 数据面与可观测对照之上的运行时安全内核层:进程模型、Sensor/Hook、TracingPolicy 与内核过滤、强制执行、事件导出与节流,并以排障与相对 Falco/CNP 的选型收束。
【Tetragon / eBPF】Sensor 与 Hook:进程传感器、kprobe、tracepoint、uprobe、LSM 与 fentry
钉清 Tetragon v1.7.0 内置 __base__ 进程传感器与 TracingPolicy 动态 hook 的分工:kprobe/kretprobe、tracepoint/rawtp、uprobe/USDT、LSM 与 fentry 各自能观察什么、依赖哪些内核能力,以及选错 hook 类型时的失败表象。