一键导入
plugin-dev
Guide for writing a new Pedro BPF plugin — structured output via .pedro_meta, build setup, and testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guide for writing a new Pedro BPF plugin — structured output via .pedro_meta, build setup, and testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Review the code and run quick checks.
Complete the required presubmit. Use before declaring a feature finished, after a large refactor or when asked.
First-time repo setup — submodules, dependencies, and verification
| name | plugin-dev |
| description | Guide for writing a new Pedro BPF plugin — structured output via .pedro_meta, build setup, and testing. |
Pedro plugins are standalone .bpf.o ELF files loaded at runtime via --plugins. They hook kernel
events, write to pedro's shared ring buffer, and may set flags on task_context to influence
policy. Structured output is declared statically in a .pedro_meta ELF section so pedro can write
typed Parquet columns without any userland code from the plugin.
Use this skill when writing a new plugin or modifying an existing one.
The canonical example is e2e/test_plugin.bpf.c. Read it first. It covers every concept below in
under 100 lines.
A plugin .bpf.c file always has four parts:
Includes — vmlinux.h first, then bpf/bpf_*.h, then:
#include "pedro-lsm/lsm/kernel/maps.h" // rb, task_map, etc.
#include "pedro/messages/plugin_meta.h" // metadata structs + column types
Including maps.h declares pedro's kernel maps. The plugin loader matches maps by name+type and
reuses pedro's fds — the plugin's declarations don't create duplicates.
.pedro_meta section — required. One pedro_plugin_meta_t in SEC(".pedro_meta"). Plugins
without it are rejected at load time.
BPF programs — SEC("lsm/...") or other attach points. All programs in the object are
attached automatically.
License — char LICENSE[] SEC("license") = "GPL";
.pedro_metaDeclare a pedro_plugin_meta_t to describe every event type the plugin emits. This drives Parquet
schema generation — column names and types come straight from here.
#define PLUGIN_ID 42 // unique across all loaded plugins; 0 is reserved
#define MY_EVENT 1
pedro_plugin_meta_t my_plugin_meta SEC(".pedro_meta") = {
.magic = PEDRO_PLUGIN_META_MAGIC,
.version = PEDRO_PLUGIN_META_VERSION,
.plugin_id = PLUGIN_ID,
.name = "my_plugin",
.event_type_count = 1,
.event_types = {{
.event_type = MY_EVENT,
.msg_kind = kMsgKindEventGenericSingle, // Half=1 slot, Single=5, Double=13
.name = "my_event", // optional; sets the writer name
.column_count = 3,
.columns = {
{.name = "pid", .type = kColumnU32, .slot = 0, .offset = 0},
{.name = "uid", .type = kColumnU32, .slot = 0, .offset = 4},
{.name = "comm", .type = kColumnString, .slot = 1},
},
}},
};
Picking a size (msg_kind):
| msg_kind | slots | struct | wire size |
|---|---|---|---|
kMsgKindEventGenericHalf | 1 | EventGenericHalf | 32 B |
kMsgKindEventGenericSingle | 5 | EventGenericSingle | 64 B |
kMsgKindEventGenericDouble | 13 | EventGenericDouble | 128 B |
Pick the smallest that fits — ring-buffer bandwidth matters.
Column types (see pedro/messages/plugin_meta.h):
kColumnU64, kColumnI64 — whole slot, offset must be 0kColumnU32, kColumnI32 — 4 bytes; offset ∈ {0, 4}kColumnU16, kColumnI16 — 2 bytes; offset ∈ {0, 2, 4, 6}kColumnString — whole slot. Strings ≤7 bytes are inlined via field.str.intern; longer strings
need Chunk delivery (see pedro-lsm/lsm/kernel/maps.h for the helpers).kColumnBytes8 — whole slot, raw 8-byte binarykColumnCookie — whole slot, write a u64 process cookie. Userland prepends the boot UUID and
stores it as a nullable string column. A cookie of 0 is written as null. If the column name ends
in _cookie, the parquet column is renamed to end in _uuid instead.Implicit columns: every plugin table starts with a common struct column (boot_uuid,
machine_id, hostname, event_time, processed_time, event_id, sensor) matching the built-in exec and
heartbeat tables. Plugin columns follow at index 1.
Sub-word packing: multiple columns can share one slot at different offsets. The pid + uid
example above packs two u32 into one 8-byte slot. Userland extracts them via the offsets — BPF just
writes the union.
Limits: 8 event types per plugin, 31 columns per event type.
Shared tables: set .flags = PEDRO_ET_SHARED and a .name on an event type to let multiple
plugins write to one table. Every plugin declaring the same shared event_type must use an
identical schema (msg_kind, name, columns) or pedro refuses to load. In BPF, emit with
ev->key.plugin_id = PEDRO_SHARED_PLUGIN_ID instead of your own id. See
e2e/test_plugin_shared.bpf.c.
Writer names (parquet output in the spool dir):
{name}{plugin.name}_{name}plugin_{id}_{event_type}Names must match [a-z][a-z0-9_-]* and be unique across all loaded plugins plus the built-in tables
(exec, heartbeat, human_readable).
Reserve from the shared rb ring buffer, fill, submit:
static inline void emit_my_event(u32 pid, u32 uid, const char *comm) {
EventGenericSingle *ev =
bpf_ringbuf_reserve(&rb, sizeof(EventGenericSingle), 0);
if (!ev) return;
__builtin_memset(ev, 0, sizeof(*ev));
ev->hdr.kind = kMsgKindEventGenericSingle;
ev->hdr.nsec_since_boot = bpf_ktime_get_boot_ns();
ev->key.plugin_id = PLUGIN_ID;
ev->key.event_type = MY_EVENT;
ev->field1.u32[0] = pid; // slot 0 offset 0
ev->field1.u32[1] = uid; // slot 0 offset 4
bpf_probe_read_kernel_str(ev->field2.str.intern,
sizeof(ev->field2.str.intern), comm);
bpf_ringbuf_submit(ev, 0);
}
GenericWord is a union: .u64, .i64, .u32[2], .i32[2], .u16[4], .i16[4], .bytes[8],
.str. Match your writes to what the metadata declares — pedro trusts the metadata, not runtime
introspection.
Plugins use the bpf_obj rule from //:bpf.bzl:
load("//:bpf.bzl", "bpf_obj")
bpf_obj(
name = "my_plugin",
src = "my_plugin.bpf.c",
hdrs = ["//pedro-lsm/lsm:kernel-headers"],
)
This produces my_plugin-bpf-obj (the .bpf.o file). For an in-tree plugin put it under e2e/
alongside test_plugin.
bazel build //e2e:my_plugin-bpf-obj
./scripts/pedro.sh -- --plugins=bazel-bin/e2e/my_plugin.bpf.o --allow-unsigned-plugins
--allow-unsigned-plugins is required unless the plugin is signed (see below). Parquet output lands
in the spool dir under the event type's writer name (see above).
Production builds embed a public key; plugins must be signed with the matching private key. Use
plugin-tool:
bazel run //bin:plugin-tool -- sign --key testdata/plugin.key my_plugin.bpf.o signed.bpf.o
bazel run //bin:plugin-tool -- verify --pubkey testdata/plugin.pub signed.bpf.o
See e2e/tests/e2e/plugin_signing.rs for the end-to-end flow.
Add a Rust e2e test under e2e/tests/e2e/. Pattern follows plugin_generic.rs:
PedroProcess::try_new with PedroArgsBuilder.plugins(vec![path])pedro.stop()pedro.parquet_reader_with_schema(WRITER_NAME, schema) (see Writer names above;
"my_plugin_my_event" for this example)Add the .bpf.o to the data list of :e2e_test in e2e/BUILD.
./scripts/quick_test.sh -a my_plugin_test_name
slot >= max_slots for the
msg_kind, or where offset + type_size > 8. These fail at load time with a clear message..pedro_meta — the plugin is rejected. The section is mandatory.bpf_ringbuf_reserve
return. sudo cat /sys/kernel/debug/tracing/trace shows bpf_printk output for debugging.