Skip to content

Commit 4662fe6

Browse files
authored
Merge pull request #105 from pythonbpf/claude/pythonbpf-state-replication-g23gij
Add kernel selftest equivalent roadmap tests
2 parents dc628ee + a5eeaaf commit 4662fe6

66 files changed

Lines changed: 1994 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/getting-started/quickstart.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def trace_open(ctx: c_void_p) -> c_int64:
188188
For network packet processing:
189189

190190
```python
191-
from pythonbpf.helper import XDP_PASS
191+
from vmlinux import XDP_PASS
192192

193193
@section("xdp")
194194
def xdp_pass(ctx: c_void_p) -> c_int64:

‎docs/user-guide/decorators.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def trace_open_return(ctx):
108108
For network packet processing at the earliest point:
109109

110110
```python
111-
from pythonbpf.helper import XDP_PASS
111+
from vmlinux import XDP_PASS
112112
from ctypes import c_void_p, c_int64
113113

114114
@section("xdp")

‎docs/user-guide/structs.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ class MyStruct:
290290
```python
291291
from pythonbpf import bpf, struct, map, section
292292
from pythonbpf.maps import RingBuffer
293-
from pythonbpf.helper import ktime, XDP_PASS
293+
from pythonbpf.helper import ktime
294+
from vmlinux import XDP_PASS
294295
from ctypes import c_void_p, c_int64, c_uint8, c_uint16, c_uint32, c_uint64
295296

296297
@bpf

‎examples/xdp_pass.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pythonbpf import bpf, map, section, bpfglobal, compile, compile_to_ir
2-
from pythonbpf.helper import XDP_PASS
2+
from vmlinux import XDP_PASS
33
from pythonbpf.maps import HashMap
44
from ctypes import c_int64, c_void_p
55

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ python_files = ["test_*.py"]
6363
markers = [
6464
"verifier: requires sudo/root for kernel verifier tests (not run by default)",
6565
"vmlinux: requires vmlinux.py for current kernel",
66+
"verifier_match: substring a verifier-level xfail expects in the rejection",
6667
]
6768
log_cli = false
6869

‎pythonbpf/allocation_pass.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pythonbpf.helper import HelperHandlerRegistry
77
from pythonbpf.vmlinux_parser.dependency_node import Field
88
from .expr import VmlinuxHandlerRegistry
9-
from pythonbpf.type_deducer import ctypes_to_ir, IntTy, signedness
9+
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy, signedness, byte_size
1010
from pythonbpf.expr.type_inference import infer_int_type
1111
from pythonbpf.maps import BPFMapType
1212

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

112112
# C type constructors
113-
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64", "c_void_p"):
113+
if is_ctypes(call_type) and isinstance(ctypes_to_ir(call_type), ir.IntType):
114+
# Any integer ctypes constructor, c_uint16 included, declares a
115+
# slot of that width; the value is converted into it at the store.
114116
ir_type = ctypes_to_ir(call_type)
115117
var = builder.alloca(ir_type, name=var_name)
116-
var.align = ir_type.width // 8
118+
var.align = byte_size(ir_type)
117119
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
118120
logger.info(f"Pre-allocated {var_name} as {call_type}")
119121

@@ -206,7 +208,7 @@ def _allocate_for_map_method(
206208
return
207209

208210
map_params = map_sym_tab[map_name].params
209-
if map_params["type"] != BPFMapType.HASH:
211+
if map_params["type"] not in (BPFMapType.HASH, BPFMapType.ARRAY):
210212
logger.warning(
211213
"Map method lookup used on non-hash map, using fallback allocation"
212214
)
@@ -470,7 +472,7 @@ def _allocate_for_attribute(
470472
tmp_name = f"{struct_var}_{field_name}_tmp"
471473
tmp_ir_type = ir.IntType(field_size_bits)
472474
tmp_var = builder.alloca(tmp_ir_type, name=tmp_name)
473-
tmp_var.align = tmp_ir_type.width // 8
475+
tmp_var.align = byte_size(tmp_ir_type)
474476
local_sym_tab[tmp_name] = LocalSymbol(tmp_var, tmp_ir_type)
475477
logger.info(
476478
f"Pre-allocated temp {tmp_name} (i{field_size_bits}) for vmlinux field read {vmlinux_struct_name}.{field_name}"
@@ -525,8 +527,8 @@ def _allocate_with_type(builder, var_name, ir_type):
525527
def _get_alignment(ir_type):
526528
"""Get appropriate alignment for IR type."""
527529
if isinstance(ir_type, ir.IntType):
528-
return ir_type.width // 8
530+
return byte_size(ir_type)
529531
elif isinstance(ir_type, ir.ArrayType) and isinstance(ir_type.element, ir.IntType):
530-
return ir_type.element.width // 8
532+
return byte_size(ir_type.element)
531533
else:
532534
return 8 # Default: pointer size

‎pythonbpf/assign_pass.py‎

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from llvmlite import ir
66
from pythonbpf.expr import eval_expr, convert
77
from pythonbpf.helper import emit_probe_read_kernel_str_call
8-
from pythonbpf.type_deducer import ctypes_to_ir
8+
from pythonbpf.type_deducer import field_int_type
99
from pythonbpf.vmlinux_parser.dependency_node import Field
1010

1111
logger = logging.getLogger(__name__)
@@ -40,6 +40,10 @@ def handle_struct_field_assignment(
4040
return
4141

4242
val, val_type = val_result
43+
if isinstance(val_type, Field):
44+
field_ty = field_int_type(val_type)
45+
if field_ty is not None:
46+
val_type = field_ty
4347

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

154165
if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
155166
# The descriptor may be narrower than the constant carrying the value
@@ -194,33 +205,13 @@ def handle_variable_assignment(
194205
)
195206
return False
196207
if isinstance(val_type, Field):
197-
logger.info("Handling assignment to struct field")
198-
field_ir_type = ctypes_to_ir(val_type.type.__name__)
199-
# Sub-register-width context fields are zero-extended to i64 by
200-
# load_ctx_field, so val is already i64 even though the field type
201-
# says otherwise (c_uint for xdp_md, c_ushort for pt_regs.cs/ss).
202-
if (
203-
isinstance(field_ir_type, ir.IntType)
204-
and field_ir_type.width < 64
205-
and isinstance(var_type, ir.IntType)
206-
and var_type.width == 64
207-
):
208-
builder.store(val, var_ptr)
209-
logger.info(
210-
f"Assigned zero-extended i{field_ir_type.width} context field "
211-
f"to {var_name} (i64)"
212-
)
213-
return True
214-
# TODO: handling only ctype struct fields for now. Handle other stuff too later.
215-
elif var_type == field_ir_type:
216-
builder.store(val, var_ptr)
217-
logger.info(f"Assigned ctype struct field to {var_name}")
218-
return True
219-
else:
220-
logger.error(
221-
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
222-
)
223-
return False
208+
# Integer fields were normalised to their IntTy above; what is
209+
# left is a pointer, array or struct field, which has no path
210+
# into this slot.
211+
logger.error(
212+
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
213+
)
214+
return False
224215
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
225216
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
226217
logger.info(

‎pythonbpf/debuginfo/debug_info_generator.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ def get_basic_type(self, name: str, size: int, encoding: int) -> Any:
4949
)
5050
return self._type_cache[key]
5151

52+
def get_int_type(self, ty) -> Any:
53+
"""Debug type for an integer IR type, by width and sign (read from the
54+
IntTy descriptor, signed for a plain ir.IntType). A 1-bit type is C's
55+
_Bool, which occupies one byte. BTF, and so a map's key and value
56+
sizes, come from this, so the width must be the real one."""
57+
from pythonbpf.type_deducer import signedness
58+
59+
width, signed = ty.width, signedness(ty)
60+
if width == 1:
61+
return self.get_basic_type("_Bool", 8, dc.DW_ATE_boolean)
62+
base = {8: "char", 16: "short", 32: "int", 64: "long long"}.get(width)
63+
if base is None:
64+
raise ValueError(f"no debug type for a {width}-bit integer")
65+
if width == 8:
66+
encoding = dc.DW_ATE_signed_char if signed else dc.DW_ATE_unsigned_char
67+
else:
68+
encoding = dc.DW_ATE_signed if signed else dc.DW_ATE_unsigned
69+
return self.get_basic_type(
70+
base if signed else f"unsigned {base}", width, encoding
71+
)
72+
5273
def get_uint8_type(self) -> Any:
5374
"""Get debug info for signed 8-bit integer"""
5475
return self.get_basic_type("char", 8, dc.DW_ATE_unsigned)

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def get_typed_operand(func, compilation_context, operand, builder, local_sym_tab
254254
vmlinux_result = VmlinuxHandlerRegistry.handle_name(operand.id)
255255
if vmlinux_result is not None:
256256
return vmlinux_result # (i64 constant, its C rank)
257+
raise SyntaxError(f"Undefined variable {operand.id}")
257258
elif isinstance(operand, ast.Constant):
258259
if isinstance(operand.value, (int, bool)):
259260
v = int(operand.value)

‎pythonbpf/functions/functions_pass.py‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
LocalSymbol,
3131
)
3232
from .function_debug_info import generate_function_debug_info
33-
from .return_utils import handle_none_return, handle_xdp_return, is_xdp_name
33+
from .return_utils import handle_none_return
3434
from .function_metadata import get_probe_string, is_global_function, infer_return_type
3535

3636

@@ -341,20 +341,6 @@ def handle_return(
341341
logger.info(f"Handling return statement: {ast.dump(stmt)}")
342342
if stmt.value is None:
343343
return handle_none_return(builder)
344-
elif (
345-
isinstance(stmt.value, ast.Name)
346-
and is_xdp_name(stmt.value.id)
347-
and stmt.value.id not in local_sym_tab
348-
and (
349-
compilation_context is None
350-
or stmt.value.id not in compilation_context.bpf_globals
351-
)
352-
):
353-
# The XDP fast path resolves names like XDP_PASS from the helper
354-
# constant table, but only as a fallback: a local or @bpfglobal of the
355-
# same name shadows it, mirroring C (a local shadows an enum constant)
356-
# and the resolution order everywhere else in the compiler.
357-
return handle_xdp_return(stmt, builder, ret_type)
358344
else:
359345
# Fallback for now if ctx not passed, but caller should pass it
360346
if compilation_context is None:

0 commit comments

Comments
 (0)