Skip to content

Commit 25106f6

Browse files
r41k0uclaude
andcommitted
Tests: Port eight kernel BPF selftests
Ports of programs from tools/testing/selftests/bpf/progs/ in the Linux tree. Each names its upstream original in a header comment. The value is breadth of program type. Between them these cover tracepoint, raw_tp, tp_btf, xdp, cgroup/getsockopt, netfilter and kprobe.multi -- most of which nothing else in the suite exercised, and all of which the section pass-through in @section was previously untested against. get_cgroup_id tracepoint/syscalls/sys_enter_nanosleep autoattach raw_tp/sys_enter + raw_tp/sys_exit link_pinning raw_tp/sys_enter + tp_btf/sys_enter tracepoint_sched_switch tracepoint/sched/sched_switch cgroup_preorder cgroup/getsockopt x4 netfilter_link_attach netfilter kprobe_multi_empty kprobe.multi/ vmlinux/xdp_devmap_helpers xdp, struct_xdp_md field access Only the BPF half of each selftest is ported. Upstream pairs every program with a userspace driver in prog_tests/ that loads it through a skeleton and asserts on the result; the framework here compiles and verifies but never runs, so these are compiler assertions rather than semantic ones. tests/passing_tests/ selftests/README.md says so explicitly. Where upstream reports results through global variables -- which most of the corpus does -- the global becomes a one-entry HashMap, tagged WORKAROUND(globals) so the substitutions can be swept up mechanically once real global variable support lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 76d69fa commit 25106f6

9 files changed

Lines changed: 373 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Ported kernel BPF selftests
2+
3+
Programs here are ports of tests from the Linux kernel tree, under
4+
`tools/testing/selftests/bpf/progs/`. Each file names its upstream original in a
5+
header comment.
6+
7+
They are ordinary cases as far as the framework is concerned: discovered
8+
automatically, and checked at all three levels (IR, `llc`, kernel verifier). The
9+
point of this directory is breadth of **program type** — between them these
10+
cover `tracepoint`, `raw_tp`, `tp_btf`, `xdp`, `cgroup/getsockopt`, `netfilter`
11+
and `kprobe.multi`, most of which nothing else in the suite exercises.
12+
13+
## What a port does and does not prove
14+
15+
A kernel selftest is two halves: the BPF program in `progs/`, and a userspace
16+
driver in `prog_tests/` that loads it through a skeleton, triggers it, and
17+
asserts on the result. **Only the BPF half is ported here**, because the
18+
framework compiles and verifies programs but never runs them.
19+
20+
So these check that PythonBPF emits a loadable, verifiable object for a given
21+
program type and feature mix. They do not check that the program *behaves* the
22+
way the kernel's version does. Treat a passing test here as a compiler
23+
assertion, not a semantic one.
24+
25+
## `WORKAROUND(globals)`
26+
27+
The selftest corpus overwhelmingly reports results through global variables: the
28+
program writes a global, and the driver reads it back. PythonBPF has no global
29+
variable support yet, so those become one-entry `HashMap`s keyed by index.
30+
31+
Every such substitution is tagged `WORKAROUND(globals)` in a comment:
32+
33+
```bash
34+
grep -rn "WORKAROUND(globals)" tests/passing_tests/selftests/
35+
```
36+
37+
This is deliberate scaffolding, not the intended shape. When real globals land,
38+
these should be rewritten to use them — the tag is there to make that sweep
39+
mechanical. Note that a map substitution also changes what a future userspace
40+
driver would read, so it is not purely cosmetic.
41+
42+
## Adding another
43+
44+
Check it against what the compiler actually supports before starting — no loops,
45+
no BPF-to-BPF calls, no kfuncs, no atomics, no `__builtin_*`, one opaque context
46+
argument, and only `HashMap` / `PerfEventArray` / `RingBuffer`. A program that
47+
needs more than one level of struct field access (`ctx->regs.ip`) is also out of
48+
reach today.
49+
50+
Anything importing from `vmlinux` goes in `vmlinux/` so it is skipped rather
51+
than failed on machines without a generated `vmlinux.py`.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_autoattach.c
2+
#
3+
# Two programs on different raw tracepoints, each recording that it ran. The
4+
# upstream test asserts both fired after bpf_object__attach_skeleton().
5+
#
6+
# WORKAROUND(globals): upstream uses `bool prog1_called` / `bool prog2_called`.
7+
# PythonBPF has no global variable support yet, so both live in one HashMap
8+
# keyed by program number. Replace with real globals once they land.
9+
10+
from pythonbpf import bpf, map, section, bpfglobal, compile
11+
from pythonbpf.maps import HashMap
12+
from ctypes import c_void_p, c_int64, c_int32, c_uint64
13+
14+
15+
# WORKAROUND(globals): key 1 -> prog1_called, key 2 -> prog2_called
16+
@bpf
17+
@map
18+
def called() -> HashMap:
19+
return HashMap(key=c_int32, value=c_uint64, max_entries=2)
20+
21+
22+
@bpf
23+
@section("raw_tp/sys_enter")
24+
def prog1(ctx: c_void_p) -> c_int64:
25+
called.update(1, 1)
26+
return c_int64(0)
27+
28+
29+
@bpf
30+
@section("raw_tp/sys_exit")
31+
def prog2(ctx: c_void_p) -> c_int64:
32+
called.update(2, 1)
33+
return c_int64(0)
34+
35+
36+
@bpf
37+
@bpfglobal
38+
def LICENSE() -> str:
39+
return "GPL"
40+
41+
42+
compile()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/cgroup_preorder.c
2+
#
3+
# Four cgroup/getsockopt programs attached at different points in a cgroup
4+
# hierarchy, each appending its own marker to a shared buffer. The upstream test
5+
# reads the buffer back to assert the order the hierarchy ran them in.
6+
#
7+
# WORKAROUND(globals): upstream uses `unsigned int idx` and `__u8 result[4]`.
8+
# PythonBPF has neither global variables nor global arrays yet, so the cursor
9+
# lives in a one-entry HashMap and the buffer in a four-entry one keyed by
10+
# position. Replace with real globals once they land.
11+
12+
from pythonbpf import bpf, map, section, bpfglobal, compile
13+
from pythonbpf.maps import HashMap
14+
from ctypes import c_void_p, c_int64, c_int32, c_uint64
15+
16+
17+
# WORKAROUND(globals): stands in for `unsigned int idx;`
18+
@bpf
19+
@map
20+
def idx() -> HashMap:
21+
return HashMap(key=c_int32, value=c_uint64, max_entries=1)
22+
23+
24+
# WORKAROUND(globals): stands in for `__u8 result[4];`, keyed by position
25+
@bpf
26+
@map
27+
def result() -> HashMap:
28+
return HashMap(key=c_int32, value=c_uint64, max_entries=4)
29+
30+
31+
@bpf
32+
@section("cgroup/getsockopt")
33+
def child(ctx: c_void_p) -> c_int64:
34+
i = idx.lookup(0)
35+
if i < 4:
36+
result.update(i, 1)
37+
idx.update(0, i + 1)
38+
return c_int64(1)
39+
40+
41+
@bpf
42+
@section("cgroup/getsockopt")
43+
def child_2(ctx: c_void_p) -> c_int64:
44+
i = idx.lookup(0)
45+
if i < 4:
46+
result.update(i, 2)
47+
idx.update(0, i + 1)
48+
return c_int64(1)
49+
50+
51+
@bpf
52+
@section("cgroup/getsockopt")
53+
def parent(ctx: c_void_p) -> c_int64:
54+
i = idx.lookup(0)
55+
if i < 4:
56+
result.update(i, 3)
57+
idx.update(0, i + 1)
58+
return c_int64(1)
59+
60+
61+
@bpf
62+
@section("cgroup/getsockopt")
63+
def parent_2(ctx: c_void_p) -> c_int64:
64+
i = idx.lookup(0)
65+
if i < 4:
66+
result.update(i, 4)
67+
idx.update(0, i + 1)
68+
return c_int64(1)
69+
70+
71+
@bpf
72+
@bpfglobal
73+
def LICENSE() -> str:
74+
return "GPL"
75+
76+
77+
compile()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/get_cgroup_id_kern.c
2+
#
3+
# Upstream records the cgroup id of a process whose pid matches one the
4+
# userspace half of the test set beforehand.
5+
#
6+
# WORKAROUND(globals): upstream uses the file-scope variables `cg_id` and
7+
# `expected_pid` to pass values in and out. PythonBPF has no global variable
8+
# support yet, so each becomes a one-entry HashMap keyed by 0. Replace these
9+
# with real globals once they land; grep for WORKAROUND(globals).
10+
11+
from pythonbpf import bpf, map, section, bpfglobal, compile
12+
from pythonbpf.maps import HashMap
13+
from pythonbpf.helper import pid, get_current_cgroup_id
14+
from ctypes import c_void_p, c_int64, c_int32, c_uint64
15+
16+
17+
# WORKAROUND(globals): stands in for `__u64 expected_pid;`
18+
@bpf
19+
@map
20+
def expected_pid() -> HashMap:
21+
return HashMap(key=c_int32, value=c_uint64, max_entries=1)
22+
23+
24+
# WORKAROUND(globals): stands in for `__u64 cg_id;`
25+
@bpf
26+
@map
27+
def cg_id() -> HashMap:
28+
return HashMap(key=c_int32, value=c_uint64, max_entries=1)
29+
30+
31+
@bpf
32+
@section("tracepoint/syscalls/sys_enter_nanosleep")
33+
def trace(ctx: c_void_p) -> c_int64:
34+
process_id = pid()
35+
want = expected_pid.lookup(0)
36+
if want == process_id:
37+
cg_id.update(0, get_current_cgroup_id())
38+
return c_int64(0)
39+
40+
41+
@bpf
42+
@bpfglobal
43+
def LICENSE() -> str:
44+
return "GPL"
45+
46+
47+
compile()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/kprobe_multi_empty.c
2+
#
3+
# An empty kprobe.multi program. Upstream attaches it to an empty symbol set to
4+
# check that path; here it checks that PythonBPF emits a loadable object for
5+
# the kprobe.multi program type, which resolves differently from plain kprobe.
6+
#
7+
# The trailing slash in the section name is upstream's and is significant: it
8+
# marks an empty symbol pattern.
9+
10+
from pythonbpf import bpf, section, bpfglobal, compile
11+
from ctypes import c_void_p, c_int64
12+
13+
14+
@bpf
15+
@section("kprobe.multi/")
16+
def test_kprobe_empty(ctx: c_void_p) -> c_int64:
17+
return c_int64(0)
18+
19+
20+
@bpf
21+
@bpfglobal
22+
def LICENSE() -> str:
23+
return "GPL"
24+
25+
26+
compile()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_link_pinning.c
2+
#
3+
# The same trivial body attached through two different program types, so the
4+
# upstream test can pin a link, close the skeleton, and check the program still
5+
# runs. Both copy an input value to an output value.
6+
#
7+
# WORKAROUND(globals): upstream uses `int in` / `int out`. PythonBPF has no
8+
# global variable support yet, so both live in one HashMap: key 0 is the input
9+
# the userspace side writes, key 1 is the output. Replace with real globals
10+
# once they land.
11+
12+
from pythonbpf import bpf, map, section, bpfglobal, compile
13+
from pythonbpf.maps import HashMap
14+
from ctypes import c_void_p, c_int64, c_int32, c_uint64
15+
16+
17+
# WORKAROUND(globals): key 0 -> in, key 1 -> out
18+
@bpf
19+
@map
20+
def io() -> HashMap:
21+
return HashMap(key=c_int32, value=c_uint64, max_entries=2)
22+
23+
24+
@bpf
25+
@section("raw_tp/sys_enter")
26+
def raw_tp_prog(ctx: c_void_p) -> c_int64:
27+
val = io.lookup(0)
28+
io.update(1, val)
29+
return c_int64(0)
30+
31+
32+
@bpf
33+
@section("tp_btf/sys_enter")
34+
def tp_btf_prog(ctx: c_void_p) -> c_int64:
35+
val = io.lookup(0)
36+
io.update(1, val)
37+
return c_int64(0)
38+
39+
40+
@bpf
41+
@bpfglobal
42+
def LICENSE() -> str:
43+
return "GPL"
44+
45+
46+
compile()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_netfilter_link_attach.c
2+
#
3+
# A netfilter program that accepts everything. Upstream uses it to exercise
4+
# link attachment against a netfilter hook; here it checks that PythonBPF emits
5+
# a loadable object for the netfilter program type.
6+
#
7+
# NF_ACCEPT is 1.
8+
9+
from pythonbpf import bpf, section, bpfglobal, compile
10+
from ctypes import c_void_p, c_int64
11+
12+
13+
@bpf
14+
@section("netfilter")
15+
def nf_link_attach_test(ctx: c_void_p) -> c_int64:
16+
return c_int64(1)
17+
18+
19+
@bpf
20+
@bpfglobal
21+
def LICENSE() -> str:
22+
return "GPL"
23+
24+
25+
compile()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_tracepoint.c
2+
#
3+
# Upstream is a bare handler on sched/sched_switch, used to prove the program
4+
# attaches to a non-syscall tracepoint. Kept faithful: the point is the
5+
# attachment surface, not the body.
6+
#
7+
# Upstream declares the tracepoint argument layout as a struct taken from
8+
# /sys/kernel/tracing/events/sched/sched_switch/format. PythonBPF does not read
9+
# tracepoint formats, so the context stays opaque.
10+
11+
from pythonbpf import bpf, section, bpfglobal, compile
12+
from ctypes import c_void_p, c_int64
13+
14+
15+
@bpf
16+
@section("tracepoint/sched/sched_switch")
17+
def oncpu(ctx: c_void_p) -> c_int64:
18+
return c_int64(0)
19+
20+
21+
@bpf
22+
@bpfglobal
23+
def LICENSE() -> str:
24+
return "GPL"
25+
26+
27+
compile()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_xdp_devmap_helpers.c
2+
#
3+
# Reads the ingress and egress interface indices out of the XDP context and
4+
# prints them with the frame length. Upstream exists to show that
5+
# ctx->egress_ifindex is only reachable with expected_attach_type
6+
# BPF_XDP_DEVMAP; here it doubles as a struct_xdp_md field-access test.
7+
8+
from pythonbpf import bpf, section, bpfglobal, compile
9+
from vmlinux import struct_xdp_md
10+
from vmlinux import XDP_PASS
11+
from ctypes import c_int64
12+
13+
14+
@bpf
15+
@section("xdp")
16+
def xdpdm_devlog(ctx: struct_xdp_md) -> c_int64:
17+
data = ctx.data
18+
data_end = ctx.data_end
19+
ingress = ctx.ingress_ifindex
20+
egress = ctx.egress_ifindex
21+
length = data_end - data
22+
print(f"devmap redirect: dev {ingress} -> dev {egress} len {length}")
23+
return c_int64(XDP_PASS)
24+
25+
26+
@bpf
27+
@bpfglobal
28+
def LICENSE() -> str:
29+
return "GPL"
30+
31+
32+
compile()

0 commit comments

Comments
 (0)