Skip to content

Commit 298c140

Browse files
committed
Tests: Port five more selftests found by re-running the corpus audit
veristat_foo (three socket programs) is clean. test_perf_link, test_enable_stats and test_cgroup_link count with __sync_fetch_and_add, which becomes a plain `x += 1` tagged WORKAROUND(atomics) for a mechanical sweep once atomics exist. connect4_dropper compares against bpf_htons(port), written out as shifts on the low 16 bits. All five pass at IR, llc and verifier level, and bring perf_event, raw_tracepoint/sys_enter, cgroup_skb/egress and cgroup/connect4 under test. The porting notes gain the third batch, the atomics tag, the current blocker histogram from the checked-in audit tool, and how to re-run it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DLMW3X7opqAPH6bjmTVkr7
1 parent e0d4d1e commit 298c140

7 files changed

Lines changed: 302 additions & 4 deletions

File tree

‎tests/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ program writes a global and the driver reads it back. Ports keep that shape with
123123
values the kernel's driver does. Anything a global cannot yet hold (arrays,
124124
structs, strings) is a roadmap test, not a workaround.
125125

126+
The one substitution still in use is `WORKAROUND(atomics)`: upstream counters
127+
incremented with `__sync_fetch_and_add` are plain `x += 1` here, tagged on the
128+
line so the sweep is mechanical once atomics land. `PORTING-NOTES.md` in that
129+
directory records every port, its rewrite if any, and why the rest of the
130+
corpus is out of reach; `tools/selftest-audit.py` regenerates that scoring.
131+
126132
Anything importing from `vmlinux` belongs in `vmlinux/`, which is registered in
127133
`VMLINUX_TEST_DIRS_PASSING` so it is skipped rather than failed where no
128134
`vmlinux.py` has been generated.

‎tests/kernel_selftest_equivalent/PORTING-NOTES.md‎

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,62 @@ type, never narrowing. It now goes through `convert()` like every other integer
139139
| `bpf_nop_bench.c` | `bpf_loop`-based benchmark macro |
140140
| `test_tcp_estats.c` | large; inlinable helpers and struct-heavy, not attempted yet |
141141

142+
## Third batch: what the re-run audit found
143+
144+
`tools/selftest-audit.py` is the corpus classifier, rebuilt and checked in. Run against
145+
the current upstream `progs/` it reports 847 real programs, 25 with no hard blocker. All
146+
but two of those 25 were already ported or are unportable for a reason a regex cannot see
147+
(`bpf_nop_bench.c` hides a loop in a macro, `test_pkt_md_access.c` type-puns narrow loads,
148+
`tracing_struct_int128.c` indexes the raw ctx array and needs bpf_testmod to load). The
149+
programs with exactly one blocker were read by hand for anything a documented rewrite
150+
could absorb. Five more ports, all passing at every level:
151+
152+
| Port | Upstream | Section | Rewrite |
153+
|---|---|---|---|
154+
| `socket/veristat_foo.py` | `veristat_foo.c` | `socket` x3 | none |
155+
| `tracing/perf_link.py` | `test_perf_link.c` | `perf_event` | `WORKAROUND(atomics)` |
156+
| `tracing/enable_stats.py` | `test_enable_stats.c` | `raw_tracepoint/sys_enter` | `WORKAROUND(atomics)` |
157+
| `cgroup/cgroup_link.py` | `test_cgroup_link.c` | `cgroup_skb/egress` x2 | `WORKAROUND(atomics)` |
158+
| `vmlinux/connect4_dropper.py` | `connect4_dropper.c` | `cgroup/connect4` | `bpf_htons` written as shifts |
159+
160+
### `WORKAROUND(atomics)`
161+
162+
Three upstream programs count with `__sync_fetch_and_add(&x, 1)`. PythonBPF has no atomic
163+
operations, so the ports do `x += 1`, a plain read-modify-write, and tag the line. As with
164+
the earlier globals tag this is scaffolding for a mechanical sweep once atomics land:
165+
166+
```bash
167+
grep -rn "WORKAROUND(atomics)" tests/kernel_selftest_equivalent/
168+
```
169+
170+
It is not a cosmetic substitution: the upstream drivers run these programs from many
171+
CPUs at once and the exact count matters there, which is precisely what a non-atomic
172+
increment loses.
173+
174+
### The blocker histogram now
175+
176+
Over 847 real programs, hard blockers only; a program usually hits several:
177+
178+
| Blocker | Programs | Share |
179+
|---|---|---|
180+
| unsupported helper | 496 | 59% |
181+
| kfuncs | 284 | 34% |
182+
| unsupported map type | 273 | 32% |
183+
| verifier-test annotations | 238 | 28% |
184+
| typed program macros (`BPF_PROG`, `BPF_KPROBE`) | 234 | 28% |
185+
| BPF-to-BPF calls | 176 | 21% |
186+
| `goto` | 143 | 17% |
187+
| inline asm | 142 | 17% |
188+
| struct globals | 126 | 15% |
189+
| CO-RE reads | 116 | 14% |
190+
| loops | 109 | 13% |
191+
| array globals | 105 | 12% |
192+
| atomics | 86 | 10% |
193+
194+
Globals no longer appear as a blocker at all. The next unlocks by count are helpers (a
195+
long tail, but `bpf_get_current_task`, `bpf_ktime_get_boot_ns` and the `bpf_probe_read_user*`
196+
family recur), array maps, and typed program arguments.
197+
142198
## 3. Incidental findings
143199

144200
- **Nested struct field access fails with a misleading error.** `ctx.regs.ip` reports
@@ -155,7 +211,11 @@ type, never narrowing. It now goes through `convert()` like every other integer
155211

156212
## Re-running the corpus scoring
157213

158-
The audit behind this spike scored all 976 programs against the compiler's envelope. It is
159-
worth re-running after each feature lands, to see what the change unlocked. The blocker
160-
histogram at the time of writing, over 820 real programs: globals 52%, verifier-test
161-
annotations 27%, typed program macros 26%, kfuncs 20%, inline asm 16%, loops 12%.
214+
```bash
215+
git clone --depth 1 --filter=blob:none --sparse https://github.com/torvalds/linux
216+
git -C linux sparse-checkout set --no-cone tools/testing/selftests/bpf/progs
217+
python3 tools/selftest-audit.py linux/tools/testing/selftests/bpf/progs --histogram --max-hard 1
218+
```
219+
220+
Re-run it after each feature lands. The envelope it encodes (helper, map and construct
221+
lists at the top of the script) is maintained by hand and must move with the compiler.
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_cgroup_link.c
2+
#
3+
# Two cgroup_skb/egress programs, each counting its own runs. Upstream
4+
# attaches one through a cgroup bpf_link, then swaps in the other with
5+
# bpf_link_update() and checks the right counter moves:
6+
#
7+
# int calls = 0;
8+
# int alt_calls = 0;
9+
#
10+
# SEC("cgroup_skb/egress")
11+
# int egress(struct __sk_buff *skb)
12+
# {
13+
# __sync_fetch_and_add(&calls, 1);
14+
# return 1;
15+
# }
16+
#
17+
# WORKAROUND(atomics): the upstream increments are atomic. PythonBPF has no
18+
# atomic operations, so these are plain read-modify-writes of the globals.
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 calls() -> c_int32:
27+
return c_int32(0)
28+
29+
30+
@bpf
31+
@bpfglobal
32+
def alt_calls() -> c_int32:
33+
return c_int32(0)
34+
35+
36+
@bpf
37+
@section("cgroup_skb/egress")
38+
def egress(skb: c_void_p) -> c_int64:
39+
global calls
40+
calls += 1 # WORKAROUND(atomics): __sync_fetch_and_add(&calls, 1)
41+
return c_int64(1)
42+
43+
44+
@bpf
45+
@section("cgroup_skb/egress")
46+
def egress_alt(skb: c_void_p) -> c_int64:
47+
global alt_calls
48+
alt_calls += 1 # WORKAROUND(atomics): __sync_fetch_and_add(&alt_calls, 1)
49+
return c_int64(1)
50+
51+
52+
@bpf
53+
@bpfglobal
54+
def LICENSE() -> str:
55+
return "GPL"
56+
57+
58+
compile()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/veristat_foo.c
2+
#
3+
# Three empty socket filters. Upstream exists only to exercise veristat's
4+
# program-name filters, so the bodies are irrelevant and the names are the
5+
# test. Ported for the same reason: three programs, one section, one object.
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 foo(ctx: c_void_p) -> c_int64:
14+
return c_int64(0)
15+
16+
17+
@bpf
18+
@section("socket")
19+
def bar(ctx: c_void_p) -> c_int64:
20+
return c_int64(0)
21+
22+
23+
@bpf
24+
@section("socket")
25+
def buz(ctx: c_void_p) -> c_int64:
26+
return c_int64(0)
27+
28+
29+
@bpf
30+
@bpfglobal
31+
def LICENSE() -> str:
32+
return "GPL"
33+
34+
35+
compile()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_enable_stats.c
2+
#
3+
# A raw tracepoint program that counts its runs. Upstream enables
4+
# BPF_STATS_RUN_TIME, triggers the program, and checks run_time_ns and
5+
# run_cnt in bpf_prog_info alongside `count`:
6+
#
7+
# __u64 count = 0;
8+
#
9+
# SEC("raw_tracepoint/sys_enter")
10+
# int test_enable_stats(void *ctx)
11+
# {
12+
# __sync_fetch_and_add(&count, 1);
13+
# return 0;
14+
# }
15+
#
16+
# WORKAROUND(atomics): the upstream increment is atomic. PythonBPF has no
17+
# atomic operations, so this is a plain read-modify-write of the global.
18+
19+
from pythonbpf import bpf, section, bpfglobal, compile
20+
from ctypes import c_void_p, c_int64, c_uint64
21+
22+
23+
@bpf
24+
@bpfglobal
25+
def count() -> c_uint64:
26+
return c_uint64(0)
27+
28+
29+
@bpf
30+
@section("raw_tracepoint/sys_enter")
31+
def test_enable_stats(ctx: c_void_p) -> c_int64:
32+
global count
33+
count += 1 # WORKAROUND(atomics): __sync_fetch_and_add(&count, 1)
34+
return c_int64(0)
35+
36+
37+
@bpf
38+
@bpfglobal
39+
def LICENSE() -> str:
40+
return "GPL"
41+
42+
43+
compile()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/test_perf_link.c
2+
#
3+
# A perf_event program that counts how often it runs. Upstream attaches it
4+
# through a perf_event bpf_link and checks `run_cnt` moved:
5+
#
6+
# int run_cnt = 0;
7+
#
8+
# SEC("perf_event")
9+
# int handler(struct pt_regs *ctx)
10+
# {
11+
# __sync_fetch_and_add(&run_cnt, 1);
12+
# return 0;
13+
# }
14+
#
15+
# WORKAROUND(atomics): the upstream increment is atomic. PythonBPF has no
16+
# atomic operations, so this is a plain read-modify-write of the global.
17+
# Replace with the atomic form once atomics land; grep for WORKAROUND(atomics).
18+
19+
from pythonbpf import bpf, section, bpfglobal, compile
20+
from ctypes import c_void_p, c_int64, c_int32
21+
22+
23+
@bpf
24+
@bpfglobal
25+
def run_cnt() -> c_int32:
26+
return c_int32(0)
27+
28+
29+
@bpf
30+
@section("perf_event")
31+
def handler(ctx: c_void_p) -> c_int64:
32+
global run_cnt
33+
run_cnt += 1 # WORKAROUND(atomics): __sync_fetch_and_add(&run_cnt, 1)
34+
return c_int64(0)
35+
36+
37+
@bpf
38+
@bpfglobal
39+
def LICENSE() -> str:
40+
return "GPL"
41+
42+
43+
compile()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/connect4_dropper.c
2+
#
3+
# A cgroup/connect4 hook that rejects TCP connects to one port, which
4+
# userspace writes into `port` before attaching:
5+
#
6+
# int port;
7+
#
8+
# SEC("cgroup/connect4")
9+
# int connect_v4_dropper(struct bpf_sock_addr *ctx)
10+
# {
11+
# if (ctx->type != SOCK_STREAM)
12+
# return VERDICT_PROCEED;
13+
# if (ctx->user_port == bpf_htons(port))
14+
# return VERDICT_REJECT;
15+
# return VERDICT_PROCEED;
16+
# }
17+
#
18+
# bpf_htons() is a byte swap, written out here as shifts on the low 16 bits;
19+
# SOCK_STREAM is 1.
20+
21+
from pythonbpf import bpf, section, bpfglobal, compile
22+
from vmlinux import struct_bpf_sock_addr
23+
from ctypes import c_int64, c_int32
24+
25+
VERDICT_REJECT = 0
26+
VERDICT_PROCEED = 1
27+
SOCK_STREAM = 1
28+
29+
30+
@bpf
31+
@bpfglobal
32+
def port() -> c_int32:
33+
return c_int32(0)
34+
35+
36+
@bpf
37+
@section("cgroup/connect4")
38+
def connect_v4_dropper(ctx: struct_bpf_sock_addr) -> c_int64:
39+
if ctx.type != 1:
40+
return c_int64(1)
41+
port_be = ((port & 0xFF) << 8) | ((port >> 8) & 0xFF)
42+
if ctx.user_port == port_be:
43+
return c_int64(0)
44+
return c_int64(1)
45+
46+
47+
@bpf
48+
@bpfglobal
49+
def LICENSE() -> str:
50+
return "GPL"
51+
52+
53+
compile()

0 commit comments

Comments
 (0)