Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0c5a9b3
Add kernel selftest equivalent roadmap tests
varun-r-mallya Jun 21, 2026
a64f01d
Tests: Register a vmlinux subdirectory under kernel_selftest_equivalent
r41k0u Aug 14, 2026
7911436
Tests: Port three kernel BPF selftests
r41k0u Aug 14, 2026
0459b3a
Tests: Add perf_skip as a nested-struct-access roadmap test
r41k0u Aug 14, 2026
507f075
Tests: Record what the first selftest porting spike found
r41k0u Aug 14, 2026
708b5af
Merge remote-tracking branch 'origin/master' into claude/pythonbpf-st…
claude Sep 24, 2026
3ffba47
Tests: Replace the WORKAROUND(globals) HashMaps with real @bpfglobal …
claude Sep 24, 2026
0bdea85
Core: Store a context field into a narrower slot through convert()
claude Sep 24, 2026
5e70b8f
Tests: Port sixteen more kernel selftests now that scalar globals exist
claude Sep 24, 2026
e0d4d1e
Tools: Check in the selftest corpus classifier
claude Sep 24, 2026
298c140
Tests: Port five more selftests found by re-running the corpus audit
claude Sep 24, 2026
598e3b4
Tests: Skip vmlinux tests only when vmlinux.py is absent
r41k0u Sep 24, 2026
cf80aad
Tools: Fix four classifier mistakes in the selftest audit
r41k0u Sep 24, 2026
057b68c
Core: Let every integer ctypes constructor declare a slot of its width
r41k0u Sep 24, 2026
efe58fc
Core: Treat an integer vmlinux field as its IntTy in assignments
r41k0u Sep 24, 2026
3de9832
Core: Implement ArrayMap as BPF_MAP_TYPE_ARRAY
r41k0u Sep 24, 2026
49e7485
Tests: Let a verifier-level xfail name the rejection it expects
r41k0u Sep 24, 2026
d649327
Docs: Refresh the audit numbers after the classifier fixes
r41k0u Sep 24, 2026
55fb6d6
Core: Export every XDP action from pythonbpf.helper
r41k0u Sep 24, 2026
3a984dd
Revert "Core: Export every XDP action from pythonbpf.helper"
r41k0u Sep 25, 2026
ad6adb1
x
r41k0u Sep 25, 2026
49ed2cf
Core: Report an unknown name in an operand as undefined
r41k0u Sep 25, 2026
d3fa7d7
Core: Print integers of any width in f-strings
r41k0u Sep 25, 2026
4e8f767
Core: Size and describe integers by their real width
r41k0u Sep 25, 2026
20a1e2f
Core: Accept c_bool as a ctypes type
r41k0u Sep 25, 2026
a5eeaaf
Tests: Context fields into slots of every width, and c_bool
r41k0u Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def trace_open(ctx: c_void_p) -> c_int64:
For network packet processing:

```python
from pythonbpf.helper import XDP_PASS
from vmlinux import XDP_PASS

@section("xdp")
def xdp_pass(ctx: c_void_p) -> c_int64:
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def trace_open_return(ctx):
For network packet processing at the earliest point:

```python
from pythonbpf.helper import XDP_PASS
from vmlinux import XDP_PASS
from ctypes import c_void_p, c_int64

@section("xdp")
Expand Down
3 changes: 2 additions & 1 deletion docs/user-guide/structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ class MyStruct:
```python
from pythonbpf import bpf, struct, map, section
from pythonbpf.maps import RingBuffer
from pythonbpf.helper import ktime, XDP_PASS
from pythonbpf.helper import ktime
from vmlinux import XDP_PASS
from ctypes import c_void_p, c_int64, c_uint8, c_uint16, c_uint32, c_uint64

@bpf
Expand Down
2 changes: 1 addition & 1 deletion examples/xdp_pass.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pythonbpf import bpf, map, section, bpfglobal, compile, compile_to_ir
from pythonbpf.helper import XDP_PASS
from vmlinux import XDP_PASS
from pythonbpf.maps import HashMap
from ctypes import c_int64, c_void_p

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ python_files = ["test_*.py"]
markers = [
"verifier: requires sudo/root for kernel verifier tests (not run by default)",
"vmlinux: requires vmlinux.py for current kernel",
"verifier_match: substring a verifier-level xfail expects in the rejection",
]
log_cli = false

Expand Down
16 changes: 9 additions & 7 deletions pythonbpf/allocation_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pythonbpf.helper import HelperHandlerRegistry
from pythonbpf.vmlinux_parser.dependency_node import Field
from .expr import VmlinuxHandlerRegistry
from pythonbpf.type_deducer import ctypes_to_ir, IntTy, signedness
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy, signedness, byte_size
from pythonbpf.expr.type_inference import infer_int_type
from pythonbpf.maps import BPFMapType

Expand Down Expand Up @@ -110,10 +110,12 @@ def _allocate_for_call(builder, var_name, rval, local_sym_tab, compilation_conte
call_type = rval.func.id

# C type constructors
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64", "c_void_p"):
if is_ctypes(call_type) and isinstance(ctypes_to_ir(call_type), ir.IntType):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is bool also accepted here?

# Any integer ctypes constructor, c_uint16 included, declares a
# slot of that width; the value is converted into it at the store.
ir_type = ctypes_to_ir(call_type)
var = builder.alloca(ir_type, name=var_name)
var.align = ir_type.width // 8
var.align = byte_size(ir_type)
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} as {call_type}")

Expand Down Expand Up @@ -206,7 +208,7 @@ def _allocate_for_map_method(
return

map_params = map_sym_tab[map_name].params
if map_params["type"] != BPFMapType.HASH:
if map_params["type"] not in (BPFMapType.HASH, BPFMapType.ARRAY):
logger.warning(
"Map method lookup used on non-hash map, using fallback allocation"
)
Expand Down Expand Up @@ -470,7 +472,7 @@ def _allocate_for_attribute(
tmp_name = f"{struct_var}_{field_name}_tmp"
tmp_ir_type = ir.IntType(field_size_bits)
tmp_var = builder.alloca(tmp_ir_type, name=tmp_name)
tmp_var.align = tmp_ir_type.width // 8
tmp_var.align = byte_size(tmp_ir_type)
local_sym_tab[tmp_name] = LocalSymbol(tmp_var, tmp_ir_type)
logger.info(
f"Pre-allocated temp {tmp_name} (i{field_size_bits}) for vmlinux field read {vmlinux_struct_name}.{field_name}"
Expand Down Expand Up @@ -525,8 +527,8 @@ def _allocate_with_type(builder, var_name, ir_type):
def _get_alignment(ir_type):
"""Get appropriate alignment for IR type."""
if isinstance(ir_type, ir.IntType):
return ir_type.width // 8
return byte_size(ir_type)
elif isinstance(ir_type, ir.ArrayType) and isinstance(ir_type.element, ir.IntType):
return ir_type.element.width // 8
return byte_size(ir_type.element)
else:
return 8 # Default: pointer size
47 changes: 19 additions & 28 deletions pythonbpf/assign_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from llvmlite import ir
from pythonbpf.expr import eval_expr, convert
from pythonbpf.helper import emit_probe_read_kernel_str_call
from pythonbpf.type_deducer import ctypes_to_ir
from pythonbpf.type_deducer import field_int_type
from pythonbpf.vmlinux_parser.dependency_node import Field

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -40,6 +40,10 @@ def handle_struct_field_assignment(
return

val, val_type = val_result
if isinstance(val_type, Field):
field_ty = field_int_type(val_type)
if field_ty is not None:
val_type = field_ty

# Special case: i8* string to [N x i8] char array
if _is_char_array(field_type) and _is_i8_ptr(val_type):
Expand Down Expand Up @@ -150,6 +154,13 @@ def handle_variable_assignment(
logger.info(
f"Evaluated value for {var_name}: {val} of type {val_type}, expected {var_type}"
)
# An integer vmlinux field is, for conversion purposes, its declared IntTy
# (width and sign from the ctype), so it takes the same convert() path as
# every other integer below instead of a special case.
if isinstance(val_type, Field):
field_ty = field_int_type(val_type)
if field_ty is not None:
val_type = field_ty

if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
# The descriptor may be narrower than the constant carrying the value
Expand Down Expand Up @@ -194,33 +205,13 @@ def handle_variable_assignment(
)
return False
if isinstance(val_type, Field):
logger.info("Handling assignment to struct field")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this change is fine? Can you comprehensively test this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine though

field_ir_type = ctypes_to_ir(val_type.type.__name__)
# Sub-register-width context fields are zero-extended to i64 by
# load_ctx_field, so val is already i64 even though the field type
# says otherwise (c_uint for xdp_md, c_ushort for pt_regs.cs/ss).
if (
isinstance(field_ir_type, ir.IntType)
and field_ir_type.width < 64
and isinstance(var_type, ir.IntType)
and var_type.width == 64
):
builder.store(val, var_ptr)
logger.info(
f"Assigned zero-extended i{field_ir_type.width} context field "
f"to {var_name} (i64)"
)
return True
# TODO: handling only ctype struct fields for now. Handle other stuff too later.
elif var_type == field_ir_type:
builder.store(val, var_ptr)
logger.info(f"Assigned ctype struct field to {var_name}")
return True
else:
logger.error(
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
)
return False
# Integer fields were normalised to their IntTy above; what is
# left is a pointer, array or struct field, which has no path
# into this slot.
logger.error(
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
)
return False
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
logger.info(
Expand Down
21 changes: 21 additions & 0 deletions pythonbpf/debuginfo/debug_info_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ def get_basic_type(self, name: str, size: int, encoding: int) -> Any:
)
return self._type_cache[key]

def get_int_type(self, ty) -> Any:
"""Debug type for an integer IR type, by width and sign (read from the
IntTy descriptor, signed for a plain ir.IntType). A 1-bit type is C's
_Bool, which occupies one byte. BTF, and so a map's key and value
sizes, come from this, so the width must be the real one."""
from pythonbpf.type_deducer import signedness

width, signed = ty.width, signedness(ty)
if width == 1:
return self.get_basic_type("_Bool", 8, dc.DW_ATE_boolean)
base = {8: "char", 16: "short", 32: "int", 64: "long long"}.get(width)
if base is None:
raise ValueError(f"no debug type for a {width}-bit integer")
if width == 8:
encoding = dc.DW_ATE_signed_char if signed else dc.DW_ATE_unsigned_char
else:
encoding = dc.DW_ATE_signed if signed else dc.DW_ATE_unsigned
return self.get_basic_type(
base if signed else f"unsigned {base}", width, encoding
)

def get_uint8_type(self) -> Any:
"""Get debug info for signed 8-bit integer"""
return self.get_basic_type("char", 8, dc.DW_ATE_unsigned)
Expand Down
1 change: 1 addition & 0 deletions pythonbpf/expr/expr_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def get_typed_operand(func, compilation_context, operand, builder, local_sym_tab
vmlinux_result = VmlinuxHandlerRegistry.handle_name(operand.id)
if vmlinux_result is not None:
return vmlinux_result # (i64 constant, its C rank)
raise SyntaxError(f"Undefined variable {operand.id}")
elif isinstance(operand, ast.Constant):
if isinstance(operand.value, (int, bool)):
v = int(operand.value)
Expand Down
16 changes: 1 addition & 15 deletions pythonbpf/functions/functions_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
LocalSymbol,
)
from .function_debug_info import generate_function_debug_info
from .return_utils import handle_none_return, handle_xdp_return, is_xdp_name
from .return_utils import handle_none_return
from .function_metadata import get_probe_string, is_global_function, infer_return_type


Expand Down Expand Up @@ -341,20 +341,6 @@ def handle_return(
logger.info(f"Handling return statement: {ast.dump(stmt)}")
if stmt.value is None:
return handle_none_return(builder)
elif (
isinstance(stmt.value, ast.Name)
and is_xdp_name(stmt.value.id)
and stmt.value.id not in local_sym_tab
and (
compilation_context is None
or stmt.value.id not in compilation_context.bpf_globals
)
):
# The XDP fast path resolves names like XDP_PASS from the helper
# constant table, but only as a fallback: a local or @bpfglobal of the
# same name shadows it, mirroring C (a local shadows an enum constant)
# and the resolution order everywhere else in the compiler.
return handle_xdp_return(stmt, builder, ret_type)
else:
# Fallback for now if ctx not passed, but caller should pass it
if compilation_context is None:
Expand Down
32 changes: 0 additions & 32 deletions pythonbpf/functions/return_utils.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,12 @@
import logging
import ast

from llvmlite import ir

logger: logging.Logger = logging.getLogger(__name__)

XDP_ACTIONS = {
"XDP_ABORTED": 0,
"XDP_DROP": 1,
"XDP_PASS": 2,
"XDP_TX": 3,
"XDP_REDIRECT": 4,
}


def handle_none_return(builder) -> bool:
"""Handle return or return None -> returns 0."""
builder.ret(ir.Constant(ir.IntType(64), 0))
logger.debug("Generated default return: 0")
return True


def is_xdp_name(name: str) -> bool:
"""Check if a name is an XDP action"""
return name in XDP_ACTIONS


def handle_xdp_return(stmt: ast.Return, builder, ret_type) -> bool:
"""Handle XDP returns"""
if not isinstance(stmt.value, ast.Name):
return False

action_name = stmt.value.id

if action_name not in XDP_ACTIONS:
raise ValueError(
f"Unknown XDP action: {action_name}. Available: {XDP_ACTIONS.keys()}"
)

value = XDP_ACTIONS[action_name]
builder.ret(ir.Constant(ret_type, value))
logger.debug(f"Generated XDP action return: {action_name} = {value}")
return True
17 changes: 3 additions & 14 deletions pythonbpf/globals_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@

from logging import Logger
import logging
from .type_deducer import ctypes_to_ir, is_signed_ctype
from .type_deducer import ctypes_to_ir, byte_size
from .symbols import BpfGlobalSymbol
from .debuginfo import DebugInfoGenerator
from .expr import VmlinuxHandlerRegistry
from .debuginfo import dwarf_constants as dc

logger: Logger = logging.getLogger(__name__)

_C_NAME_BY_WIDTH = {8: "char", 16: "short", 32: "int", 64: "long long"}


def populate_global_symbol_table(tree, compilation_context):
"""
Expand Down Expand Up @@ -78,7 +75,7 @@ def _emit_global(module: ir.Module, node, name):
# (align 4 for i32, align 8 for i64). llc derives the BTF DATASEC layout
# from these symbols, so the alignment should mirror the C reference in
# tests/c-form/global_vars.bpf.c.
gvar.align = ty.width // 8 if isinstance(ty, ir.IntType) else 8
gvar.align = byte_size(ty) if isinstance(ty, ir.IntType) else 8
gvar.linkage = "dso_local"
gvar.global_constant = False
return gvar
Expand All @@ -93,15 +90,7 @@ def _emit_global_debug_info(compilation_context, gvar, name, ctype_name):
future skeleton can tell which variable lives at which offset.
"""
generator = DebugInfoGenerator(compilation_context.module)
width = gvar.value_type.width
signed = is_signed_ctype(ctype_name)
base = _C_NAME_BY_WIDTH[width]
if width == 8:
encoding = dc.DW_ATE_signed_char if signed else dc.DW_ATE_unsigned_char
else:
encoding = dc.DW_ATE_signed if signed else dc.DW_ATE_unsigned
cname = base if signed else f"unsigned {base}"
di_type = generator.get_basic_type(cname, width, encoding)
di_type = generator.get_int_type(ctypes_to_ir(ctype_name))
dv = generator.create_global_var_debug_info(name, di_type, is_local=False)
gvar.set_metadata("dbg", dv)

Expand Down
4 changes: 0 additions & 4 deletions pythonbpf/helper/__init__.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait what is this? XDP_TX, XDP_ABORTED and XDP_PASS (everything XDP_..) is from the vmlinux.py. Why is this being included from helpers?

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
skb_store_bytes,
get_current_cgroup_id,
get_stack,
XDP_DROP,
XDP_PASS,
)


Expand Down Expand Up @@ -86,6 +84,4 @@ def helper_call_handler(call, compilation_context, builder, func, local_sym_tab)
"uid",
"skb_store_bytes",
"get_stack",
"XDP_DROP",
"XDP_PASS",
]
7 changes: 0 additions & 7 deletions pythonbpf/helper/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,3 @@ def get_stack(buf, flags=0):
def get_current_cgroup_id():
"""Get the current cgroup ID"""
return ctypes.c_int64(0)


XDP_ABORTED = ctypes.c_int64(0)
XDP_DROP = ctypes.c_int64(1)
XDP_PASS = ctypes.c_int64(2)
XDP_TX = ctypes.c_int64(3)
XDP_REDIRECT = ctypes.c_int64(4)
16 changes: 5 additions & 11 deletions pythonbpf/helper/printk_formatter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ast
import logging
from pythonbpf.type_deducer import signedness

from llvmlite import ir
from pythonbpf.expr import eval_expr, get_base_type_and_depth, deref_to_depth, convert
Expand Down Expand Up @@ -155,17 +156,10 @@ def _process_attr_in_fval(attr_node, fmt_parts, exprs, local_sym_tab, struct_sym
def _populate_fval(ftype, node, fmt_parts, exprs):
"""Populate format parts and expressions based on field type."""
if isinstance(ftype, ir.IntType):
# TODO: We print as signed integers only for now
if ftype.width == 64:
fmt_parts.append("%lld")
exprs.append(node)
elif ftype.width == 32:
fmt_parts.append("%d")
exprs.append(node)
else:
raise NotImplementedError(
f"Unsupported integer width in f-string: {ftype.width}"
)
# Every integer argument is widened to 64 bits (per its sign) before
# the call, so the 64-bit format fits any width; the sign picks it.
fmt_parts.append("%lld" if signedness(ftype) else "%llu")
exprs.append(node)
elif isinstance(ftype, ir.PointerType):
target, depth = get_base_type_and_depth(ftype)
if isinstance(target, ir.IntType):
Expand Down
11 changes: 9 additions & 2 deletions pythonbpf/maps/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from .maps import HashMap, PerfEventArray, RingBuffer
from .maps import ArrayMap, HashMap, PerfEventArray, RingBuffer
from .maps_pass import maps_proc
from .map_types import BPFMapType

__all__ = ["HashMap", "PerfEventArray", "maps_proc", "RingBuffer", "BPFMapType"]
__all__ = [
"ArrayMap",
"HashMap",
"PerfEventArray",
"maps_proc",
"RingBuffer",
"BPFMapType",
]
Loading
Loading