Skip to content

Commit 5e70b8f

Browse files
committed
Tests: Port sixteen more kernel selftests now that scalar globals exist
Every program on the selftest audit's Tier 1 and Tier 2 lists that the compiler can express today: xdp_dummy, priv_prog, test_xdp_link, xdp_tx, tc_dummy, test_tc_bpf, cgroup_mprog, cgroup_skb_direct_packet_access, test_signed_loader, test_signed_loader_data, test_netfilter_link_attach, kprobe_multi_empty, uprobe_multi_bench, uprobe_multi_usdt, test_link_pinning and test_xdp_devmap_helpers. Fifteen pass at every level, bringing tc, tcx, cgroup/getsockopt, cgroup_skb, socket, netfilter, kprobe.multi, uprobe.multi, usdt and tp_btf program types under test for the first time. test_xdp_devmap_helpers is a negative fixture upstream: reading egress_ifindex is only legal with expected_attach_type = BPF_XDP_DEVMAP and the driver asserts the plain load fails. It is a strict verifier-level xfail here for the same reason, until expected_attach_type can be expressed. The porting notes record the batch and why each remaining program on the two lists is still out of reach. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DLMW3X7opqAPH6bjmTVkr7
1 parent 0bdea85 commit 5e70b8f

18 files changed

Lines changed: 578 additions & 0 deletions

‎tests/kernel_selftest_equivalent/PORTING-NOTES.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,54 @@ cursor, which together constrain the design more than anything here does.
9191
enums, then gives up), a Python-level surface for declaring one, and userspace access
9292
through `pylibbpf`.
9393

94+
## Second batch: everything portable after globals
95+
96+
With scalar globals in, the audit's Tier 1 and Tier 2 lists were re-read against the
97+
compiler and every program it can express was ported. Sixteen more, fifteen of which
98+
pass at every level; the sixteenth passes at IR and llc and is rejected by the verifier
99+
exactly as its upstream driver asserts it must be:
100+
101+
| Port | Upstream | Section | Outcome |
102+
|---|---|---|---|
103+
| `xdp/xdp_dummy.py` | `xdp_dummy.c` | `xdp` x2 | passes |
104+
| `xdp/priv_prog.py` | `priv_prog.c` | `xdp` | passes |
105+
| `xdp/xdp_link.py` | `test_xdp_link.c` | `xdp`, `tc` | passes |
106+
| `vmlinux/xdp_tx.py` | `xdp_tx.c` | `xdp` | passes |
107+
| `tc/tc_dummy.py` | `tc_dummy.c` | `tc` | passes |
108+
| `vmlinux/tc_bpf.py` | `test_tc_bpf.c` | `tc`, `tcx/ingress` | passes (direct packet access) |
109+
| `cgroup/cgroup_mprog.py` | `cgroup_mprog.c` | `cgroup/getsockopt` x4 | passes |
110+
| `vmlinux/cgroup_skb_direct_packet_access.py` | `cgroup_skb_direct_packet_access.c` | `cgroup_skb/ingress` | passes, after a compiler fix |
111+
| `socket/signed_loader.py` | `test_signed_loader.c` | `socket` | passes |
112+
| `socket/signed_loader_data.py` | `test_signed_loader_data.c` | `socket` | passes (.data global) |
113+
| `netfilter/netfilter_link_attach.py` | `test_netfilter_link_attach.c` | `netfilter` | passes |
114+
| `tracing/kprobe_multi_empty.py` | `kprobe_multi_empty.c` | `kprobe.multi/` | passes |
115+
| `tracing/uprobe_multi_bench.py` | `uprobe_multi_bench.c` | `uprobe.multi/...` | passes (`count += 1`) |
116+
| `tracing/uprobe_multi_usdt.py` | `uprobe_multi_usdt.c` | `usdt` | passes |
117+
| `tracing/link_pinning.py` | `test_link_pinning.c` | `raw_tp/sys_enter`, `tp_btf/sys_enter` | passes |
118+
| `vmlinux/xdp_devmap_helpers.py` | `test_xdp_devmap_helpers.c` | `xdp` | verifier xfail by design |
119+
120+
**One compiler bug fell out.** `data_end = skb->data_end` into a `__u32` global failed
121+
with `cannot store i64 to i32*`: context fields are loaded widened to i64, and the
122+
assignment path only accepted them into 64-bit slots or slots of exactly the field's
123+
type, never narrowing. It now goes through `convert()` like every other integer store.
124+
`passing_tests/vmlinux/ctx_field_narrow_store.py` pins it.
125+
126+
**What is still not portable, and why**, from the same two lists:
127+
128+
| Program | Blocker |
129+
|---|---|
130+
| `metadata_used.c`, `metadata_unused.c` | `char[]` `.rodata` globals: only integer scalars can be globals |
131+
| `test_log_buf.c`, `cgroup_preorder.c`, `uprobe_multi_pid_filter.c`, `test_build_id.c` | array globals |
132+
| `token_kallsyms.c`, `test_btf_ext.c`, `test_static_linked*.c` | BPF-to-BPF calls (`__weak` / `__noinline` subprogs) |
133+
| `test_trace_ext.c`, `freplace_get_constant.c` | `freplace` needs a target program to load against |
134+
| `test_subskeleton*.c` | extern symbols, `__kconfig`, static linking |
135+
| `test_pkt_md_access.c` | narrow type-punned loads of `__sk_buff` fields |
136+
| `test_xdp_attach_fail.c` | tracepoint `__data_loc` pointer arithmetic on a custom ctx struct |
137+
| `sockopt_multi.c` | writes to context fields and through `optval` |
138+
| `tracing_struct_many_args.c` | `BPF_PROG2` multi-argument entry |
139+
| `bpf_nop_bench.c` | `bpf_loop`-based benchmark macro |
140+
| `test_tcp_estats.c` | large; inlinable helpers and struct-heavy, not attempted yet |
141+
94142
## 3. Incidental findings
95143

96144
- **Nested struct field access fails with a misleading error.** `ctx.regs.ip` reports
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/cgroup_mprog.c
2+
#
3+
# Four identical cgroup/getsockopt programs. Upstream attaches them in
4+
# various orders with BPF_F_BEFORE/BPF_F_AFTER and checks the resulting
5+
# multi-prog chain; the bodies only need to exist and return "allow".
6+
7+
from pythonbpf import bpf, section, bpfglobal, compile
8+
from ctypes import c_void_p, c_int64
9+
10+
11+
@bpf
12+
@section("cgroup/getsockopt")
13+
def getsockopt_1(ctx: c_void_p) -> c_int64:
14+
return c_int64(1)
15+
16+
17+
@bpf
18+
@section("cgroup/getsockopt")
19+
def getsockopt_2(ctx: c_void_p) -> c_int64:
20+
return c_int64(1)
21+
22+
23+
@bpf
24+
@section("cgroup/getsockopt")
25+
def getsockopt_3(ctx: c_void_p) -> c_int64:
26+
return c_int64(1)
27+
28+
29+
@bpf
30+
@section("cgroup/getsockopt")
31+
def getsockopt_4(ctx: c_void_p) -> c_int64:
32+
return c_int64(1)
33+
34+
35+
@bpf
36+
@bpfglobal
37+
def LICENSE() -> str:
38+
return "GPL"
39+
40+
41+
compile()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_netfilter_link_attach.c
2+
#
3+
# A netfilter program that accepts everything (NF_ACCEPT is 1). Upstream
4+
# attaches it with every combination of protocol family, hook and priority
5+
# and checks which the kernel rejects.
6+
7+
from pythonbpf import bpf, section, bpfglobal, compile
8+
from ctypes import c_void_p, c_int64
9+
10+
11+
@bpf
12+
@section("netfilter")
13+
def nf_link_attach_test(ctx: c_void_p) -> c_int64:
14+
return c_int64(1)
15+
16+
17+
@bpf
18+
@bpfglobal
19+
def LICENSE() -> str:
20+
return "GPL"
21+
22+
23+
compile()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_signed_loader.c
2+
#
3+
# A minimal, map-less socket filter. Upstream drives it through libbpf's
4+
# light-skeleton loader to test signed-program loading; a socket filter
5+
# needs no attach resolution and no maps keeps the loader trivial.
6+
7+
from pythonbpf import bpf, section, bpfglobal, compile
8+
from ctypes import c_void_p, c_int64
9+
10+
11+
@bpf
12+
@section("socket")
13+
def probe(ctx: c_void_p) -> c_int64:
14+
return c_int64(0)
15+
16+
17+
@bpf
18+
@bpfglobal
19+
def LICENSE() -> str:
20+
return "GPL"
21+
22+
23+
compile()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_signed_loader_data.c
2+
#
3+
# The signed-loader fixture with one initialised global, so the object has a
4+
# .data map that the loader must seed. Upstream checks that a signed loader
5+
# keeps the attested initial value:
6+
#
7+
# __u64 magic = 0x5eed1234abad1deaULL;
8+
#
9+
# SEC("socket")
10+
# int probe(void *ctx)
11+
# {
12+
# return (int)magic;
13+
# }
14+
15+
from pythonbpf import bpf, section, bpfglobal, compile
16+
from ctypes import c_void_p, c_int32, c_uint64
17+
18+
19+
@bpf
20+
@bpfglobal
21+
def magic() -> c_uint64:
22+
return c_uint64(0x5EED1234ABAD1DEA)
23+
24+
25+
@bpf
26+
@section("socket")
27+
def probe(ctx: c_void_p) -> c_int32:
28+
return c_int32(magic)
29+
30+
31+
@bpf
32+
@bpfglobal
33+
def LICENSE() -> str:
34+
return "GPL"
35+
36+
37+
compile()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/tc_dummy.c
2+
#
3+
# A classifier that returns TC_ACT_OK for everything. Upstream is the fixture
4+
# behind the tc_links and tc_opts attach-order tests.
5+
6+
from pythonbpf import bpf, section, bpfglobal, compile
7+
from ctypes import c_void_p, c_int64
8+
9+
10+
@bpf
11+
@section("tc")
12+
def entry(skb: c_void_p) -> c_int64:
13+
return c_int64(1)
14+
15+
16+
@bpf
17+
@bpfglobal
18+
def LICENSE() -> str:
19+
return "GPL"
20+
21+
22+
compile()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/kprobe_multi_empty.c
2+
#
3+
# An empty kprobe.multi program. Upstream attaches it to every function in
4+
# the kernel's available_filter_functions list to benchmark attach time.
5+
6+
from pythonbpf import bpf, section, bpfglobal, compile
7+
from ctypes import c_void_p, c_int64
8+
9+
10+
@bpf
11+
@section("kprobe.multi/")
12+
def test_kprobe_empty(ctx: c_void_p) -> c_int64:
13+
return c_int64(0)
14+
15+
16+
@bpf
17+
@bpfglobal
18+
def LICENSE() -> str:
19+
return "GPL"
20+
21+
22+
compile()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_link_pinning.c
2+
#
3+
# Two programs, one raw_tp and one tp_btf on the same tracepoint, each
4+
# copying a global set by userspace into a global read by userspace.
5+
# Upstream pins the link, closes every fd, then checks the program still
6+
# fires by bumping `in` and watching `out` follow:
7+
#
8+
# int in = 0;
9+
# int out = 0;
10+
#
11+
# SEC("raw_tp/sys_enter")
12+
# int raw_tp_prog(const void *ctx)
13+
# {
14+
# out = in;
15+
# return 0;
16+
# }
17+
#
18+
# `in` is renamed `in_val` because `in` is a Python keyword.
19+
20+
from pythonbpf import bpf, section, bpfglobal, compile
21+
from ctypes import c_void_p, c_int64, c_int32
22+
23+
24+
@bpf
25+
@bpfglobal
26+
def in_val() -> c_int32:
27+
return c_int32(0)
28+
29+
30+
@bpf
31+
@bpfglobal
32+
def out() -> c_int32:
33+
return c_int32(0)
34+
35+
36+
@bpf
37+
@section("raw_tp/sys_enter")
38+
def raw_tp_prog(ctx: c_void_p) -> c_int64:
39+
global out
40+
out = in_val
41+
return c_int64(0)
42+
43+
44+
@bpf
45+
@section("tp_btf/sys_enter")
46+
def tp_btf_prog(ctx: c_void_p) -> c_int64:
47+
global out
48+
out = in_val
49+
return c_int64(0)
50+
51+
52+
@bpf
53+
@bpfglobal
54+
def LICENSE() -> str:
55+
return "GPL"
56+
57+
58+
compile()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/uprobe_multi_bench.c
2+
#
3+
# Count how many times the multi-uprobe fires. Upstream attaches it to
4+
# thousands of uprobe_multi_func_* symbols and reads `count` back:
5+
#
6+
# int count;
7+
#
8+
# SEC("uprobe.multi/./uprobe_multi:uprobe_multi_func_*")
9+
# int uprobe_bench(struct pt_regs *ctx)
10+
# {
11+
# count++;
12+
# return 0;
13+
# }
14+
15+
from pythonbpf import bpf, section, bpfglobal, compile
16+
from ctypes import c_void_p, c_int64, c_int32
17+
18+
19+
@bpf
20+
@bpfglobal
21+
def count() -> c_int32:
22+
return c_int32(0)
23+
24+
25+
@bpf
26+
@section("uprobe.multi/./uprobe_multi:uprobe_multi_func_*")
27+
def uprobe_bench(ctx: c_void_p) -> c_int64:
28+
global count
29+
count += 1
30+
return c_int64(0)
31+
32+
33+
@bpf
34+
@bpfglobal
35+
def LICENSE() -> str:
36+
return "GPL"
37+
38+
39+
compile()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/uprobe_multi_usdt.c
2+
#
3+
# The USDT flavour of the multi-uprobe counter. Upstream attaches it to a
4+
# USDT probe in the test binary and asserts on `count` from userspace.
5+
6+
from pythonbpf import bpf, section, bpfglobal, compile
7+
from ctypes import c_void_p, c_int64, c_int32
8+
9+
10+
@bpf
11+
@bpfglobal
12+
def count() -> c_int32:
13+
return c_int32(0)
14+
15+
16+
@bpf
17+
@section("usdt")
18+
def usdt0(ctx: c_void_p) -> c_int64:
19+
global count
20+
count += 1
21+
return c_int64(0)
22+
23+
24+
@bpf
25+
@bpfglobal
26+
def LICENSE() -> str:
27+
return "GPL"
28+
29+
30+
compile()

0 commit comments

Comments
 (0)