بنقرة واحدة
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.