From f796338245bc867ed249ff699492a25145b6df04 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 05:37:05 +0530 Subject: [PATCH 1/9] Tests: Silence deprecation warnings in test-cov and test-verifier c2de7da added -W ignore::DeprecationWarning to the 'test' target only, so 'make test-cov' and 'make test-verifier' still drown in warnings from the generated vmlinux.py. Also correct the test-verifier note: the suite does not run pytest under sudo, tests/framework/verifier.py shells out to 'sudo bpftool' itself. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7874b44e..5e3be67b 100644 --- a/Makefile +++ b/Makefile @@ -10,12 +10,13 @@ test: pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier" test-cov: - pytest tests/ -v --tb=short -m "not verifier" \ + pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier" \ --cov=pythonbpf --cov-report=term-missing --cov-report=html test-verifier: - @echo "NOTE: verifier tests require sudo and bpftool. Uses sudo .venv/bin/python3." - pytest tests/test_verifier.py -v --tb=short -m verifier + @echo "NOTE: verifier tests shell out to 'sudo bpftool'; run 'sudo -v' first so" + @echo " the timestamp does not lapse mid-run. bpftool must be installed." + pytest tests/test_verifier.py -W ignore::DeprecationWarning -v --tb=short -m verifier all: clean install From edefe626d097f48c1e6af6c3aac349cc3d6c48da Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 05:37:05 +0530 Subject: [PATCH 2/9] Tests: Use a real program section in return, var_rval and if These three declared @section("sometag1"), which is not a section name libbpf can map to a program type: libbpf: failed to guess program type from ELF section 'sometag1' so they were rejected before the kernel verifier ever saw them. They generate IR and compile fine; only the section was wrong. Switch to tracepoint/syscalls/sys_enter_execve, matching the other tracepoint tests. Co-Authored-By: Claude Opus 5 (1M context) --- tests/failing_tests/if.py | 2 +- tests/passing_tests/return.py | 2 +- tests/passing_tests/var_rval.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/failing_tests/if.py b/tests/failing_tests/if.py index 638c2ce3..8949d253 100644 --- a/tests/failing_tests/if.py +++ b/tests/failing_tests/if.py @@ -3,7 +3,7 @@ @bpf -@section("sometag1") +@section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: if 3 + 2 == 5: return c_int64(5) diff --git a/tests/passing_tests/return.py b/tests/passing_tests/return.py index 9bd048b6..67a0bb05 100644 --- a/tests/passing_tests/return.py +++ b/tests/passing_tests/return.py @@ -3,7 +3,7 @@ @bpf -@section("sometag1") +@section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: return c_int64(1 - 1) diff --git a/tests/passing_tests/var_rval.py b/tests/passing_tests/var_rval.py index ee1735e7..0742c040 100644 --- a/tests/passing_tests/var_rval.py +++ b/tests/passing_tests/var_rval.py @@ -5,7 +5,7 @@ @bpf -@section("sometag1") +@section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: a = 1 - 1 return c_int64(a) From a7e2bc39efc9656e7989cd7061ae3f0cf0842e80 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 05:39:49 +0530 Subject: [PATCH 3/9] Tests: Add a verifier level to the expected-failure tiers test_config.toml could only declare a failure at the "ir" or "llc" level, and test_verifier.py dropped every declared-xfail case from the level-3 run outright. A program that generates IR and compiles cleanly but that the kernel verifier rejects therefore had no way to be declared: it was silently treated as must-pass at all three levels. Levels now form an ordered pipeline (ir < llc < verifier) and a declared level marks that level and every later one xfail, which is what the old ir-implies-llc special case was expressing. Level 3 runs every test file and reports declared failures as expected ones rather than skipping them. Co-Authored-By: Claude Opus 5 (1M context) --- tests/README.md | 12 ++++++++++-- tests/conftest.py | 22 ++++++++++++++-------- tests/framework/bpf_test_case.py | 16 +++++++++++++++- tests/test_verifier.py | 17 +++++++++++------ 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/tests/README.md b/tests/README.md index 2861f4f3..6b63fd45 100644 --- a/tests/README.md +++ b/tests/README.md @@ -69,8 +69,16 @@ Known-broken tests are declared in `tests/test_config.toml`: "failing_tests/my_test.py" = {reason = "...", level = "ir"} ``` -- `level = "ir"` — fails during IR generation; both IR and LLC tests are marked xfail. -- `level = "llc"` — IR generates fine but `llc` rejects it; only the LLC test is marked xfail. +- `level = "ir"` — fails during IR generation. +- `level = "llc"` — IR generates fine but `llc` rejects it. +- `level = "verifier"` — IR and `llc` both succeed, but the kernel verifier rejects the object. + +A failure at one level implies failure at every later one, so the declared level marks +that level **and all later ones** xfail. An `"ir"` entry is xfail at all three levels; a +`"verifier"` entry is xfail at level 3 only and must still pass levels 1 and 2. + +Every test file runs at every level, including the ones declared here — level 3 does not +skip declared failures, it reports them as expected ones. All xfails use `strict = True`: if a test starts **passing** it shows up as **XPASS** and is treated as a test failure. This is intentional — it means the bug was fixed and the test should be promoted to `passing_tests/`. diff --git a/tests/conftest.py b/tests/conftest.py index ce92d1dd..42ab30ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ import pytest +from tests.framework.bpf_test_case import level_index from tests.framework.collector import collect_all_test_files # ── vmlinux availability ──────────────────────────────────────────────────── @@ -70,14 +71,19 @@ def pytest_collection_modifyitems(items): # xfail (strict: XPASS counts as a test failure, alerting us to fixed bugs) if case.is_expected_fail: - # Level "ir" → fails at IR generation: xfail both IR and LLC tests - # Level "llc" → IR succeeds but LLC fails: only xfail the LLC test - is_llc_test = item.nodeid.startswith("tests/test_llc_compilation.py") - - apply_xfail = (case.xfail_level == "ir") or ( - case.xfail_level == "llc" and is_llc_test - ) - if apply_xfail: + # A failure at one level implies failure at every later one, so mark + # this item xfail whenever the declared level is at or before it: + # "ir" → IR, LLC and verifier + # "llc" → LLC and verifier (IR is expected to succeed) + # "verifier" → verifier only + if item.nodeid.startswith("tests/test_verifier.py"): + item_level = "verifier" + elif item.nodeid.startswith("tests/test_llc_compilation.py"): + item_level = "llc" + else: + item_level = "ir" + + if level_index(case.xfail_level) <= level_index(item_level): item.add_marker( pytest.mark.xfail( reason=case.xfail_reason, diff --git a/tests/framework/bpf_test_case.py b/tests/framework/bpf_test_case.py index d80a7134..a993166e 100644 --- a/tests/framework/bpf_test_case.py +++ b/tests/framework/bpf_test_case.py @@ -1,6 +1,20 @@ from dataclasses import dataclass from pathlib import Path +# The three test levels, in pipeline order. A test declared as failing at one +# level is also expected to fail at every later level: a program that cannot +# generate IR cannot reach llc, and one that llc rejects never reaches the +# kernel. Used by conftest to decide which items to mark xfail. +LEVELS = ("ir", "llc", "verifier") + + +def level_index(level: str) -> int: + """Position of a level in the pipeline. Unknown levels sort first ("ir").""" + try: + return LEVELS.index(level) + except ValueError: + return 0 + @dataclass class BpfTestCase: @@ -8,7 +22,7 @@ class BpfTestCase: rel_path: str is_expected_fail: bool = False xfail_reason: str = "" - xfail_level: str = "ir" # "ir" or "llc" + xfail_level: str = "ir" # one of LEVELS needs_vmlinux: bool = False skip_reason: str = "" diff --git a/tests/test_verifier.py b/tests/test_verifier.py index 413ef453..3966e3f6 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -24,12 +24,12 @@ from tests.framework.verifier import verify_object -def _passing_test_files(): - return [c.path for c in collect_all_test_files() if not c.is_expected_fail] +def _verifier_test_files(): + return [c.path for c in collect_all_test_files()] -def _passing_test_ids(): - return [c.rel_path for c in collect_all_test_files() if not c.is_expected_fail] +def _verifier_test_ids(): + return [c.rel_path for c in collect_all_test_files()] def _get_rejection_reason(verifier_test_file: Path, output) -> str: @@ -43,11 +43,16 @@ def _get_rejection_reason(verifier_test_file: Path, output) -> str: return errstr +# Every test file runs at this level, including the ones declared in +# test_config.toml. conftest marks those xfail, so an "ir"- or "llc"-level +# failure is still reported as an expected failure rather than being silently +# dropped from the level-3 run — and a "verifier"-level entry becomes possible +# at all. @pytest.mark.verifier @pytest.mark.parametrize( "verifier_test_file", - _passing_test_files(), - ids=_passing_test_ids(), + _verifier_test_files(), + ids=_verifier_test_ids(), ) def test_kernel_verifier(verifier_test_file: Path, tmp_path, caplog): """Compile the BPF test and verify it passes the kernel verifier.""" From ecb277469439db526a86d9b08e2a96a304312fe6 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 16:55:39 +0530 Subject: [PATCH 4/9] Tests: Declare the ringbuf map with its real type and a legal size The map was declared as RingBuf, which is not an exported name -- the class is RingBuffer. maps_pass has no processor registered under RingBuf, so it fell through to the 'unknown map type, defaulting to HashMap' path and was emitted as BPF_MAP_TYPE_HASH with no key or value size. The kernel rejected it: libbpf: map 'mymap': found type = 1. libbpf: map 'mymap': failed to create: -EINVAL Nothing caught this earlier because the test framework compiles the file's AST and never imports it, so the bogus 'from pythonbpf.maps import RingBuf' never raised ImportError. Using the real name also subjects the map to process_ringbuf_map's validation, which max_entries=1024 does not survive: a ringbuf needs a power of two at least as large as the page size. Raised to 4096. libbpf now reports type = 27 with max_entries = 4096. Co-Authored-By: Claude Opus 5 (1M context) --- tests/passing_tests/ringbuf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/passing_tests/ringbuf.py b/tests/passing_tests/ringbuf.py index 0566d855..05652cf1 100644 --- a/tests/passing_tests/ringbuf.py +++ b/tests/passing_tests/ringbuf.py @@ -1,13 +1,13 @@ from pythonbpf import bpf, BPF, map, bpfglobal, section, compile, compile_to_ir -from pythonbpf.maps import RingBuf, HashMap +from pythonbpf.maps import RingBuffer, HashMap from ctypes import c_int32, c_void_p # Define a map @bpf @map -def mymap() -> RingBuf: - return RingBuf(max_entries=(1024)) +def mymap() -> RingBuffer: + return RingBuffer(max_entries=4096) @bpf From 79c8a1e1948d654d8bdad8082d591535b94927eb Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 16:57:51 +0530 Subject: [PATCH 5/9] Core: Reject an unknown map type instead of silently emitting a hash map process_bpf_map logged a warning and fell back to process_hash_map for any map type it did not recognise. That turns a typo into an object file that compiles cleanly, carries the wrong BPF_MAP_TYPE, skips the intended map type's parameter validation entirely, and is only rejected once it reaches the kernel -- as tests/passing_tests/ringbuf.py was, with 'failed to create: -EINVAL'. A warning was not enough to surface it either: the test framework only fails a test on logging.ERROR records. Raise instead, naming the registered map types so the fix is obvious: ValueError: Unknown map type 'RingBuf' returned by 'm'. Known map types: HashMap, PerfEventArray, RingBuffer Co-Authored-By: Claude Opus 5 (1M context) --- pythonbpf/maps/maps_pass.py | 16 +++++++++++----- pythonbpf/maps/maps_utils.py | 5 +++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pythonbpf/maps/maps_pass.py b/pythonbpf/maps/maps_pass.py index 083b24a6..7e3b1ed3 100644 --- a/pythonbpf/maps/maps_pass.py +++ b/pythonbpf/maps/maps_pass.py @@ -175,10 +175,16 @@ def process_bpf_map(func_node, compilation_context): if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name): handler = MapProcessorRegistry.get_processor(rval.func.id) - if handler: - return handler(map_name, rval, compilation_context) - else: - logger.warning(f"Unknown map type {rval.func.id}, defaulting to HashMap") - return process_hash_map(map_name, rval, compilation_context) + if handler is None: + # Falling back to a hash map here used to produce an object that + # compiled cleanly and was then rejected by the kernel, because the + # map carried the wrong type and none of the real map type's + # validation ran. A misspelled map type is a program error. + known = ", ".join(sorted(MapProcessorRegistry.known_types())) + raise ValueError( + f"Unknown map type '{rval.func.id}' returned by '{map_name}'. " + f"Known map types: {known}" + ) + return handler(map_name, rval, compilation_context) else: raise ValueError("Function under @map must return a map") diff --git a/pythonbpf/maps/maps_utils.py b/pythonbpf/maps/maps_utils.py index 194b408e..decf04b3 100644 --- a/pythonbpf/maps/maps_utils.py +++ b/pythonbpf/maps/maps_utils.py @@ -33,3 +33,8 @@ def decorator(func): def get_processor(cls, map_type_name): """Get the processor function for a map type""" return cls._processors.get(map_type_name) + + @classmethod + def known_types(cls): + """Names of every registered map type, for error messages""" + return list(cls._processors) From d66b468f39f43613cd8cd1547314dca03a4f41c8 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 16:57:54 +0530 Subject: [PATCH 6/9] Tests: Promote if.py to passing_tests It generates IR, compiles, and now passes the kernel verifier, so it is no longer a failing test. It sat in failing_tests/ with no test_config.toml entry, which meant the framework already required it to pass at every level -- the directory was merely misleading. Co-Authored-By: Claude Opus 5 (1M context) --- tests/{failing_tests => passing_tests}/if.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{failing_tests => passing_tests}/if.py (100%) diff --git a/tests/failing_tests/if.py b/tests/passing_tests/if.py similarity index 100% rename from tests/failing_tests/if.py rename to tests/passing_tests/if.py From fd2412e733f11fd9b7137d9c6f662b8c59d9d8ba Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 7 Aug 2026 17:00:23 +0530 Subject: [PATCH 7/9] Tests: Declare xdp_test_1 as a verifier-level expected failure It generates IR and compiles, and the CO-RE relocations against struct xdp_md resolve correctly, so it must keep passing levels 1 and 2. The kernel rejects it: the 'data + 34 < data_end' guard is emitted as a signed compare over values round-tripped through the stack, so the verifier never narrows the packet range and the later iph.saddr read fails with invalid access to packet, off=26 size=4, R1(id=0,off=26,r=0) R1 offset is outside of the packet Direct packet access needs bounds checks in a form the verifier can follow. strict = True, so this flags up if that ever lands. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_config.toml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_config.toml b/tests/test_config.toml index edcd90ce..8a6255a3 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -2,15 +2,27 @@ # # [xfail] — tests expected to fail. # key = path relative to tests/ -# value = {reason = "...", level = "ir" | "llc"} -# level "ir" = fails during pythonbpf IR generation (exception or ERROR log) -# level "llc" = IR generates but llc rejects it +# value = {reason = "...", level = "ir" | "llc" | "verifier"} +# level "ir" = fails during pythonbpf IR generation (exception or ERROR log) +# level "llc" = IR generates but llc rejects it +# level "verifier" = IR and llc both succeed, but the kernel verifier rejects it +# +# A failure at one level implies failure at every later one, so the declared +# level marks that level and all later ones xfail. # [xfail] "failing_tests/conditionals/struct_ptr.py" = {reason = "Struct pointer used directly as boolean condition not supported", level = "ir"} +# Compiles cleanly; rejected by the kernel. The `data + 34 < data_end` guard is +# emitted as a signed compare over values round-tripped through the stack, so the +# verifier never narrows the packet range (it stays r=0) and the later +# `iph.saddr` read fails with "invalid access to packet, off=26 size=4" / +# "R1 offset is outside of the packet". Direct packet access needs bounds checks +# in a form the verifier can follow. +"failing_tests/xdp/xdp_test_1.py" = {reason = "XDP direct packet access: the data/data_end guard does not establish a packet range the verifier can use", level = "verifier"} + "failing_tests/license.py" = {reason = "Missing LICENSE global produces IR that llc rejects — should be caught earlier with a clear error message", level = "llc"} "failing_tests/undeclared_values.py" = {reason = "Undeclared variable used in f-string — should raise SyntaxError (correct behaviour, test documents it)", level = "ir"} From e1b40d2c778712af171315e0605fe75256011459 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi <76248539+r41k0u@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:04:28 +0530 Subject: [PATCH 8/9] Fix comment in maps_pass.py --- pythonbpf/maps/maps_pass.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pythonbpf/maps/maps_pass.py b/pythonbpf/maps/maps_pass.py index 7e3b1ed3..ca078454 100644 --- a/pythonbpf/maps/maps_pass.py +++ b/pythonbpf/maps/maps_pass.py @@ -176,10 +176,9 @@ def process_bpf_map(func_node, compilation_context): if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name): handler = MapProcessorRegistry.get_processor(rval.func.id) if handler is None: - # Falling back to a hash map here used to produce an object that - # compiled cleanly and was then rejected by the kernel, because the - # map carried the wrong type and none of the real map type's - # validation ran. A misspelled map type is a program error. + # Raise an exception and fail the build because the + # map carried the wrong type. A misspelled map type + # is a program error. known = ", ".join(sorted(MapProcessorRegistry.known_types())) raise ValueError( f"Unknown map type '{rval.func.id}' returned by '{map_name}'. " From 180c35f83ed6d7c5d0044b6c75398c86336e0db9 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi <76248539+r41k0u@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:06:06 +0530 Subject: [PATCH 9/9] just return the keys instead of all the processors in maps_utils,py --- pythonbpf/maps/maps_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonbpf/maps/maps_utils.py b/pythonbpf/maps/maps_utils.py index decf04b3..a271697c 100644 --- a/pythonbpf/maps/maps_utils.py +++ b/pythonbpf/maps/maps_utils.py @@ -37,4 +37,4 @@ def get_processor(cls, map_type_name): @classmethod def known_types(cls): """Names of every registered map type, for error messages""" - return list(cls._processors) + return list(cls._processors.keys())