Skip to content

Commit de259c8

Browse files
authored
Merge pull request #109 from pythonbpf/tests/loop-selftests
Port selftests for loop (non-volatile) which are feasible now
2 parents ec5d72f + 9974491 commit de259c8

8 files changed

Lines changed: 286 additions & 11 deletions

File tree

‎tests/framework/verifier.py‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,34 @@
33
from collections import namedtuple
44
from pathlib import Path
55

6+
Output = namedtuple("Output", ["stdout", "stderr"])
67

7-
def verify_object(obj_path: Path) -> tuple[bool, str]:
8-
"""Run bpftool prog load -d to verify a BPF object file against the kernel verifier.
8+
9+
def verify_object(obj_path: Path) -> tuple[bool, Output]:
10+
"""Run bpftool prog load to verify a BPF object file against the kernel verifier.
911
1012
Pins the program temporarily at /sys/fs/bpf/bpf_prog_test_<uuid>, then removes it.
11-
Returns (success, combined_output). Requires sudo / root.
13+
Returns (success, Output(stdout, stderr)). Requires sudo / root.
14+
15+
No -d: it asks the kernel for the full instruction-level log (log level
16+
1+2+4) on every load. For a program that makes the verifier walk a loop that
17+
log outgrows libbpf's buffer, the kernel then fails the load with ENOSPC, and
18+
libbpf doubles the buffer and verifies the whole program again, until the
19+
load takes longer than the timeout. A rejection still comes with its log:
20+
libbpf retries a failed load at log level 1 and prints the log as a warning.
1221
"""
1322
pin_path = f"/sys/fs/bpf/bpf_prog_test_{uuid.uuid4().hex[:8]}"
1423
try:
1524
result = subprocess.run(
16-
["sudo", "bpftool", "prog", "load", "-d", str(obj_path), pin_path],
25+
["sudo", "bpftool", "prog", "load", str(obj_path), pin_path],
1726
capture_output=True,
1827
text=True,
1928
timeout=30,
2029
)
21-
Output = namedtuple("Output", ["stdout", "stderr"])
22-
output = Output(stdout=result.stdout, stderr=result.stderr)
23-
return result.returncode == 0, output
30+
return result.returncode == 0, Output(
31+
stdout=result.stdout, stderr=result.stderr
32+
)
2433
except subprocess.TimeoutExpired:
25-
return False, "bpftool timed out after 30s"
34+
return False, Output(stdout="", stderr="bpftool timed out after 30s")
2635
finally:
2736
subprocess.run(["sudo", "rm", "-f", pin_path], check=False, capture_output=True)

‎tests/kernel_selftest_equivalent/PORTING-NOTES.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,41 @@ Globals no longer appear as a blocker at all. The next unlocks by count are help
200200
long tail, but `bpf_get_current_task`, `bpf_ktime_get_boot_ns` and the `bpf_probe_read_user*`
201201
family recur), array maps, and typed program arguments.
202202

203+
## Fourth batch: loops
204+
205+
With `for`/`while` lowering (#106) the audit no longer counts a plain loop as a hard
206+
blocker; the bpf_for/bpf_repeat iterators and the `bpf_loop` helper still are (38
207+
programs), and `do { } while (0)` macros are no longer mistaken for loops. Against
208+
bpf-next `5714ca8` that takes the programs with no hard blocker from 29 to 39. Four of
209+
the ten are ported, all from `prog_tests/bpf_verif_scale.c`:
210+
211+
| Port | Upstream | Section | Upstream BPF insns | Port BPF insns |
212+
|---|---|---|---|---|
213+
| `vmlinux/loop1.py` | `loop1.c` | `raw_tracepoint/kfree_skb` | 21 | 20 |
214+
| `vmlinux/loop2.py` | `loop2.c` | `raw_tracepoint/consume_skb` | 12 | 2 |
215+
| `vmlinux/loop4.py` | `loop4.c` | `socket` | 14 | 97 |
216+
| `vmlinux/loop5.py` | `loop5.c` | `socket` | 18 | 13 |
217+
218+
The loop lowering matches clang's: with `volatile`, `barrier()` and the no-unroll pragma
219+
taken out of the C, clang emits the same 2 and 97 instructions for `loop2` and `loop4`.
220+
Those three are what the ports cannot say, and they are the point of the upstream tests,
221+
which exist to make the verifier walk a loop. `loop2` folds to `return 42` and `loop4`
222+
unrolls, so those two ports compile, load and verify but no longer exercise what their
223+
upstream counterparts do. Each says so in its header; they are the first tests to
224+
tighten once volatile reads have a spelling. `loop1` and `loop5` keep their loops.
225+
226+
The other six stay unported:
227+
228+
- `loop3.c` is a negative test (the verifier must give up on a 2^32-iteration loop), and
229+
without `volatile` LLVM replaces that loop with a closed form.
230+
- `test_skb_ctx.c` needs a subscript on a context array (`skb->cb[i]`) and writes to
231+
context fields. Today the first fails with a bare `TypeError` in `handle_cond` and the
232+
second with a `KeyError` in `handle_struct_field_assignment`.
233+
- `test_verif_scale1.c`, `2` and `3` contain no loop, only a `do { } while (0)` macro;
234+
they need `jhash` over packet bytes through casts, and `scale1` a `noinline` subprogram.
235+
- `test_parse_tcp_hdr_opt.c` reads packet bytes through casts and passes a struct pointer
236+
to a static function.
237+
203238
## 3. Incidental findings
204239

205240
- **Nested struct field access fails with a misleading error.** `ctx.regs.ip` reports
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/loop1.c
2+
#
3+
# A verifier-scale test (prog_tests/bpf_verif_scale.c, test_verif_scale_loop1):
4+
# two nested bounded loops, the inner one bounded by the outer counter, so the
5+
# verifier has to prove termination of ~45000 iterations. Upstream:
6+
#
7+
# SEC("raw_tracepoint/kfree_skb")
8+
# int nested_loops(volatile struct pt_regs* ctx)
9+
# {
10+
# int i, j, sum = 0, m;
11+
#
12+
# for (j = 0; j < 300; j++)
13+
# for (i = 0; i < j; i++) {
14+
# if (j & 1)
15+
# m = PT_REGS_RC(ctx);
16+
# else
17+
# m = j;
18+
# sum += i * m;
19+
# }
20+
#
21+
# return sum;
22+
# }
23+
#
24+
# PT_REGS_RC is ctx->ax on x86. The `volatile` on ctx, which forces a reload of
25+
# ctx->ax on every iteration, has no PythonBPF spelling yet; without it LLVM may
26+
# hoist the load out of the loops. Here it does not (the load sits in a branch),
27+
# and the object keeps upstream's nested-loop shape: 20 instructions against
28+
# clang's 21, with 64-bit loop counters where C's are 32-bit.
29+
30+
from pythonbpf import bpf, section, bpfglobal, compile
31+
from vmlinux import struct_pt_regs
32+
from ctypes import c_int32
33+
34+
35+
@bpf
36+
@section("raw_tracepoint/kfree_skb")
37+
def nested_loops(ctx: struct_pt_regs) -> c_int32:
38+
total: c_int32 = 0
39+
m: c_int32 = 0
40+
for j in range(300):
41+
for i in range(j):
42+
if j & 1:
43+
m = ctx.ax
44+
else:
45+
m = j
46+
total += i * m
47+
return total
48+
49+
50+
@bpf
51+
@bpfglobal
52+
def LICENSE() -> str:
53+
return "GPL"
54+
55+
56+
compile()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/loop2.c
2+
#
3+
# A verifier-scale test (prog_tests/bpf_verif_scale.c, test_verif_scale_loop2):
4+
# an unbounded-looking `while (true)` that only terminates through a `break`,
5+
# with a step that depends on the context. Upstream:
6+
#
7+
# SEC("raw_tracepoint/consume_skb")
8+
# int while_true(volatile struct pt_regs* ctx)
9+
# {
10+
# int i = 0;
11+
#
12+
# while (true) {
13+
# if (PT_REGS_RC(ctx) & 1)
14+
# i += 3;
15+
# else
16+
# i += 7;
17+
# if (i > 40)
18+
# break;
19+
# }
20+
#
21+
# return i;
22+
# }
23+
#
24+
# PT_REGS_RC is ctx->ax on x86.
25+
#
26+
# NOTE: this port does not test what upstream tests. `volatile` has no
27+
# PythonBPF spelling yet, so ctx->ax is loaded once and LLVM sees that both
28+
# paths end at 42 (3 * 14 and 7 * 6): the program compiles to `return 42` and
29+
# the verifier never sees a loop. Clang does the same to the C with `volatile`
30+
# removed. Revisit when volatile reads land.
31+
32+
from pythonbpf import bpf, section, bpfglobal, compile
33+
from vmlinux import struct_pt_regs
34+
from ctypes import c_int32
35+
36+
37+
@bpf
38+
@section("raw_tracepoint/consume_skb")
39+
def while_true(ctx: struct_pt_regs) -> c_int32:
40+
i: c_int32 = 0
41+
while True:
42+
if ctx.ax & 1:
43+
i += 3
44+
else:
45+
i += 7
46+
if i > 40:
47+
break
48+
return i
49+
50+
51+
@bpf
52+
@bpfglobal
53+
def LICENSE() -> str:
54+
return "GPL"
55+
56+
57+
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/loop4.c
2+
#
3+
# A verifier-scale test (prog_tests/bpf_verif_scale.c, test_verif_scale_loop4):
4+
# a 20-iteration loop whose body branches on a context field, so the verifier
5+
# sees 2^20 paths unless it prunes states. Upstream:
6+
#
7+
# SEC("socket")
8+
# int combinations(volatile struct __sk_buff* skb)
9+
# {
10+
# int ret = 0, i;
11+
#
12+
# __pragma_loop_no_unroll
13+
# for (i = 0; i < 20; i++)
14+
# if (skb->len)
15+
# ret |= 1 << i;
16+
# return ret;
17+
# }
18+
#
19+
# NOTE: this port does not test what upstream tests. `volatile` and the
20+
# no-unroll pragma have no PythonBPF spelling, so skb->len is loaded once and
21+
# LLVM fully unrolls the loop (97 instructions against upstream's 14): the
22+
# verifier walks straight-line code instead of pruning states across a loop.
23+
# Clang does the same to the C with `volatile` and the pragma removed. Revisit
24+
# when volatile reads land.
25+
26+
from pythonbpf import bpf, section, bpfglobal, compile
27+
from vmlinux import struct___sk_buff
28+
from ctypes import c_int32
29+
30+
31+
@bpf
32+
@section("socket")
33+
def combinations(skb: struct___sk_buff) -> c_int32:
34+
ret: c_int32 = 0
35+
for i in range(20):
36+
if skb.len:
37+
ret |= 1 << i
38+
return ret
39+
40+
41+
@bpf
42+
@bpfglobal
43+
def LICENSE() -> str:
44+
return "GPL"
45+
46+
47+
compile()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Ported from Linux tools/testing/selftests/bpf/progs/loop5.c
2+
#
3+
# A verifier-scale test (prog_tests/bpf_verif_scale.c, test_verif_scale_loop5):
4+
# `while (1)` with several exits, where whether an exit is ever reached depends
5+
# on a context field. Upstream:
6+
#
7+
# SEC("socket")
8+
# int while_true(volatile struct __sk_buff* skb)
9+
# {
10+
# int i = 0;
11+
#
12+
# while (1) {
13+
# if (skb->len)
14+
# i += 3;
15+
# else
16+
# i += 7;
17+
# if (i == 9)
18+
# break;
19+
# barrier();
20+
# if (i == 10)
21+
# break;
22+
# barrier();
23+
# if (i == 13)
24+
# break;
25+
# barrier();
26+
# if (i == 14)
27+
# break;
28+
# }
29+
# return i;
30+
# }
31+
#
32+
# barrier() is an empty `asm volatile("" ::: "memory")` that stops clang from
33+
# merging the exits; PythonBPF has no compiler barrier, and LLVM merges the four
34+
# exits into one bitmask test. The loop survives, so the verifier still has to
35+
# prove it terminates.
36+
37+
from pythonbpf import bpf, section, bpfglobal, compile
38+
from vmlinux import struct___sk_buff
39+
from ctypes import c_int32
40+
41+
42+
@bpf
43+
@section("socket")
44+
def while_true(skb: struct___sk_buff) -> c_int32:
45+
i: c_int32 = 0
46+
while True:
47+
if skb.len:
48+
i += 3
49+
else:
50+
i += 7
51+
if i == 9:
52+
break
53+
if i == 10:
54+
break
55+
if i == 13:
56+
break
57+
if i == 14:
58+
break
59+
return i
60+
61+
62+
@bpf
63+
@bpfglobal
64+
def LICENSE() -> str:
65+
return "GPL"
66+
67+
68+
compile()

‎tests/test_verifier.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Level 3 — Kernel verifier tests.
33
44
For every passing BPF test file, compiles to a .o and runs:
5-
sudo bpftool prog load -d <file.o> /sys/fs/bpf/bpf_prog_test_<id>
5+
sudo bpftool prog load <file.o> /sys/fs/bpf/bpf_prog_test_<id>
66
77
These tests are opt-in: they require sudo and kernel access, and are gated
88
behind the `verifier` pytest mark. Run with:

‎tools/selftest-audit.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
construct lists below) must be kept in step with the compiler by hand. Re-run
1616
it after a feature lands to see what the feature unlocked. The lists were last
1717
reconciled against pythonbpf/helper and pythonbpf/maps when scalar @bpfglobal
18-
support and integer signedness merged.
18+
support, integer signedness and loops merged.
1919
"""
2020

2121
import argparse
@@ -70,7 +70,10 @@
7070
# cost a careful rewrite can absorb). Order does not matter.
7171
PATTERNS = [
7272
# control flow
73-
("loop", "hard", r"\b(for|while)\s*\(|\bbpf_for\b|\bbpf_repeat\b|\bbpf_loop\s*\("),
73+
# plain for/while lower since loops landed (#106); `do { } while (0)` is a
74+
# macro idiom, not a loop. Iterator loops and the bpf_loop helper are not.
75+
("open_coded_loop", "hard", r"\bbpf_for(_each)?\b|\bbpf_repeat\b|\bbpf_loop\s*\("),
76+
("do_while", "soft", r"\}\s*while\s*\((?!\s*0\s*\))"),
7477
("goto", "hard", r"\bgoto\s+\w+"),
7578
("switch", "hard", r"\bswitch\s*\("),
7679
("ternary", "soft", r"\?[^?:]*:"),

0 commit comments

Comments
 (0)