diff --git a/Makefile b/Makefile index 5e3be67b..fe3332ef 100644 --- a/Makefile +++ b/Makefile @@ -6,18 +6,29 @@ clean: rm -rf examples/*.ll examples/*.o rm -rf htmlcov .coverage -test: +# Regenerate the master vmlinux.py from the running kernel's BTF, then +# symlink it into every directory under tests/ so both pytest (which +# resolves "import vmlinux" via pythonpath=["."]) and any test file run +# standalone from its own directory see the same, always-fresh fixture. +vmlinux: + python3 tools/vmlinux-gen.py -o vmlinux.py + @find tests -type d -not -path '*/__pycache__*' | while read -r d; do \ + target=$$(python3 -c "import os,sys; print(os.path.relpath('vmlinux.py', sys.argv[1]))" "$$d"); \ + ln -sf "$$target" "$$d/vmlinux.py"; \ + done + +test: vmlinux pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier" -test-cov: +test-cov: vmlinux pytest tests/ -W ignore::DeprecationWarning -v --tb=short -m "not verifier" \ --cov=pythonbpf --cov-report=term-missing --cov-report=html -test-verifier: +test-verifier: vmlinux @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 -.PHONY: all clean install test test-cov test-verifier +.PHONY: all clean install test test-cov test-verifier vmlinux diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 89e87af6..69be3230 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -403,7 +403,7 @@ def process_bpf_chunk(func_node, compilation_context, return_type): if func_node.args.args: # Only look at the first argument for now param = func.args[0] - param.add_attribute("nocapture") + param.add_attribute("captures(none)") probe_string = get_probe_string(func_node) if probe_string is not None: diff --git a/tests/framework/collector.py b/tests/framework/collector.py index c40da231..bdafc149 100644 --- a/tests/framework/collector.py +++ b/tests/framework/collector.py @@ -35,6 +35,10 @@ def collect_all_test_files() -> list[BpfTestCase]: cases = [] for subdir in ("passing_tests", "failing_tests"): for py_file in sorted((TESTS_DIR / subdir).rglob("*.py")): + if py_file.name == "vmlinux.py": + # Not a test case: the per-directory symlink to the master + # vmlinux fixture module, kept alongside tests that import it. + continue rel = str(py_file.relative_to(TESTS_DIR)) needs_vmlinux = _is_vmlinux_test(rel) diff --git a/tools/vmlinux-gen.py b/tools/vmlinux-gen.py index 714d1ce8..ec9c97ee 100755 --- a/tools/vmlinux-gen.py +++ b/tools/vmlinux-gen.py @@ -261,11 +261,109 @@ def repl(m): for name in invalid_ctypes: data = re.sub(rf"\bctypes\.{name}\b", name, data) + data = self.disambiguate_anonymous_field_names(data) + with open(self.output_file, "w") as f: f.write(data) self.log(f"Saved final output to {self.output_file}") + def disambiguate_anonymous_field_names(self, data): + """Make clang2py's generic '_N' field names globally unique per struct/union. + + clang2py names both (a) unnamed anonymous struct/union members that go + into `_anonymous_`, and (b) other unnamed/reserved members, using the + same per-struct sequential scheme ('_0', '_1', ...). ctypes flattens + anonymous members by promoting their field names onto the parent class, + so if a struct has an anonymous member (say '_0') whose type itself has + a field also named '_1', and that same struct has ANOTHER anonymous + member named '_1', the promoted name collides with the parent's own + field key. ctypes then fails with a cryptic + "type object 'c_ulong' has no attribute '_fields_'" (or similar) when + `_fields_` is assigned. Prefixing every generic '_N' key with its + owning struct/union name keeps these names unique across the whole + file, so promoted names can never collide with a sibling's own key. + """ + self.log("Disambiguating generic anonymous-field names...") + + def sanitize(name): + return re.sub(r"[^0-9A-Za-z_]", "_", name) + + lines = data.split("\n") + class_re = re.compile(r"^class\s+(\w+)\(") + stmt_fields_re = re.compile(r"^(\w+)\._fields_\s*=\s*\[\s*$") + inline_fields_re = re.compile(r"^\s+_fields_\s*=\s*\[\s*$") + anon_stmt_re = re.compile(r"^(\w+)\._anonymous_\s*=\s*\((.*)\)\s*$") + anon_inline_re = re.compile(r"^(\s+)_anonymous_\s*=\s*\((.*)\)\s*$") + key_re = re.compile(r"(\('_)([0-9]+)(',)") + + last_class = None + out = [] + i = 0 + n = len(lines) + renamed_count = 0 + while i < n: + line = lines[i] + + m_class = class_re.match(line) + if m_class: + last_class = m_class.group(1) + out.append(line) + i += 1 + continue + + m_anon = anon_stmt_re.match(line) + if m_anon: + cls, body = m_anon.group(1), m_anon.group(2) + prefix = sanitize(cls) + new_body, cnt = re.subn( + r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body + ) + renamed_count += cnt + out.append(f"{cls}._anonymous_ = ({new_body})") + i += 1 + continue + + m_anon_inline = anon_inline_re.match(line) + if m_anon_inline and last_class: + indent, body = m_anon_inline.group(1), m_anon_inline.group(2) + prefix = sanitize(last_class) + new_body, cnt = re.subn( + r"'_([0-9]+)'", lambda mm: f"'_{prefix}_{mm.group(1)}'", body + ) + renamed_count += cnt + out.append(f"{indent}_anonymous_ = ({new_body})") + i += 1 + continue + + m_stmt = stmt_fields_re.match(line) + m_inline = inline_fields_re.match(line) if not m_stmt else None + if m_stmt or m_inline: + cls = m_stmt.group(1) if m_stmt else last_class + prefix = sanitize(cls) if cls else None + out.append(line) + i += 1 + while i < n and lines[i].strip() != "]": + fl = lines[i] + if prefix: + fl, cnt = key_re.subn( + lambda mm: f"{mm.group(1)}{prefix}_{mm.group(2)}{mm.group(3)}", + fl, + ) + renamed_count += cnt + out.append(fl) + i += 1 + if i < n: + out.append(lines[i]) # closing ']' + i += 1 + continue + + out.append(line) + i += 1 + + self.log(f"Renamed {renamed_count} generic field keys") + return "\n".join(out) + def cleanup(self): """Remove temporary files if not keeping them.""" if not self.keep_intermediate and self.temp_dir != ".":