From fab456b83d393efdd3891a7d19a17cf0e949e798 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 25 Sep 2026 11:20:13 +0530 Subject: [PATCH 1/5] Tests: Pin 32-bit arithmetic on xdp_md.data as a verifier-level xfail xdp_md.data is u32 in C, so ctx.data - k lowers to a 32-bit add on the loaded value, exactly as C ranks it, and the verifier rejects it: it tracks data as a packet pointer and prohibits 32-bit pointer arithmetic (observed in CI). IR and llc succeed, so the entry is at the verifier level. It flips to passing when packet-pointer fields get 64-bit pointer rank (TODO in expr_pass._descriptor). Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/failing_tests/vmlinux/ctx_data_arith.py | 27 +++++++++++++++++++ tests/test_config.toml | 4 +++ 2 files changed, 31 insertions(+) create mode 100644 tests/failing_tests/vmlinux/ctx_data_arith.py diff --git a/tests/failing_tests/vmlinux/ctx_data_arith.py b/tests/failing_tests/vmlinux/ctx_data_arith.py new file mode 100644 index 00000000..d09a5d6d --- /dev/null +++ b/tests/failing_tests/vmlinux/ctx_data_arith.py @@ -0,0 +1,27 @@ +# xdp_md.data is declared u32, so C's rules make ctx.data - k a 32-bit +# subtraction, and that is what the compiler emits (w0 += -1). The verifier +# tracks data as a packet pointer and rejects it: "R0 32-bit pointer +# arithmetic prohibited". C hits the same and casts through (void *)(long). +# The planned fix gives the packet-pointer fields 64-bit pointer rank so no +# cast is needed; see the TODO in expr_pass._descriptor. The same u32 rank on +# a scalar field is pinned as passing in passing_tests/vmlinux/ctx_field_rank.py. +from ctypes import c_int32, c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + k = c_int32(1) + d = ctx.data - k + return c_int64(d) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/test_config.toml b/tests/test_config.toml index 6f9bec08..3f81509f 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -60,3 +60,7 @@ "failing_tests/loops/for_continue.py" = {reason = "for/while loops not implemented: continue is not supported", level = "ir"} "failing_tests/loops/for_map_items.py" = {reason = "for/while loops not implemented: no sugar over bpf_for_each_map_elem()-style map iteration exists yet", level = "ir"} + +# 32-bit arithmetic on a packet-pointer context field. IR and llc succeed; the +# verifier rejects it with "R0 32-bit pointer arithmetic prohibited". +"failing_tests/vmlinux/ctx_data_arith.py" = {reason = "xdp_md.data is u32 in C but a packet pointer to the verifier; 32-bit arithmetic on it is prohibited until packet-pointer fields get 64-bit pointer rank", level = "verifier"} From 6a859ac0cd70fd9995e7e20d74e38eb7dcc7dd0d Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 25 Sep 2026 11:58:42 +0530 Subject: [PATCH 2/5] Core: Special-case packet-pointer context fields in arithmetic xdp_md.data/data_end/data_meta and __sk_buff.data/data_end/data_meta are u32 in C but packet pointers to the verifier, which rewrites their load into a pointer load and prohibits 32-bit arithmetic on them. The signedness rules ranked them as u32, so ctx.data - k lowered to a 32-bit add (w0 += -1) and was rejected; C code avoids this with a cast through (void *)(long). A binary operation with a packet-pointer operand now bypasses the usual arithmetic conversions: the pointer is used at 64 bits, an offset added to or subtracted from it is taken as an unsigned 16-bit value (truncated, zero-extended) because the verifier only tracks packet ranges up to MAX_PACKET_OFF (0xffff), and the result is a 64-bit value. Pointer minus pointer is an ordinary 64-bit length. A constant offset outside 0..65535, any other operator, and offset minus pointer are compile errors rather than code the verifier would reject. Local inference agrees (64-bit unsigned). Only direct field reads are recognised; a local copied from one is an integer again, and comparisons between packet pointers are the next site of the same rule. Co-Authored-By: Claude Opus 5.5 (1M context) --- pythonbpf/expr/expr_pass.py | 52 +++++++++++++++++++ pythonbpf/expr/packet_pointer.py | 46 ++++++++++++++++ pythonbpf/expr/type_inference.py | 5 ++ tests/failing_tests/vmlinux/ctx_data_arith.py | 27 ---------- .../vmlinux/ctx_data_offset_too_big.py | 21 ++++++++ tests/passing_tests/vmlinux/ctx_data_arith.py | 26 ++++++++++ .../passing_tests/vmlinux/ctx_data_length.py | 21 ++++++++ .../vmlinux/ctx_data_plus_one.py | 21 ++++++++ .../vmlinux/ctx_data_runtime_offset.py | 24 +++++++++ tests/test_config.toml | 6 +-- tests/test_signedness_ir.py | 20 +++++++ 11 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 pythonbpf/expr/packet_pointer.py delete mode 100644 tests/failing_tests/vmlinux/ctx_data_arith.py create mode 100644 tests/failing_tests/vmlinux/ctx_data_offset_too_big.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_arith.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_length.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_plus_one.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_runtime_offset.py diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index c454e704..b74a4ded 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -24,6 +24,7 @@ get_base_type_and_depth, ) from .vmlinux_registry import VmlinuxHandlerRegistry +from .packet_pointer import MAX_PACKET_OFF, is_packet_pointer from ..vmlinux_parser.dependency_node import Field logger: Logger = logging.getLogger(__name__) @@ -284,12 +285,18 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t narrowed to it -- so u32 * u32 wraps at 32 bits even though the arithmetic itself runs in an i64 register. Returns (value, IntTy).""" op = rval.op + left_pkt = is_packet_pointer(rval.left, local_sym_tab) + right_pkt = is_packet_pointer(rval.right, local_sym_tab) left, left_ty = get_typed_operand( func, compilation_context, rval.left, builder, local_sym_tab ) right, right_ty = get_typed_operand( func, compilation_context, rval.right, builder, local_sym_tab ) + if left_pkt or right_pkt: + return _packet_pointer_binop( + builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt + ) result_ty = usual_arithmetic_conversions(left_ty, right_ty) logger.info( f"binop {type(op).__name__}: {left_ty.describe()} x {right_ty.describe()} " @@ -301,6 +308,51 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t return canonicalise(builder, result, result_ty), result_ty +def _packet_pointer_binop( + builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt +): + """A binary operation with a packet-pointer operand (see packet_pointer.py): + 64-bit, never ranked as the field's declared u32, with the offset taken as + an unsigned 16-bit value so the verifier can track the packet range.""" + op = rval.op + where = f"line {rval.lineno}: {ast.unparse(rval)}" + ptr_ty = IntTy(64, False) + if left_pkt and right_pkt: + if isinstance(op, ast.Sub): + # data_end - data: a length, an ordinary 64-bit scalar. + left = convert(builder, left, left_ty, ptr_ty) + right = convert(builder, right, right_ty, ptr_ty) + return builder.sub(left, right), ptr_ty + raise SyntaxError( + f"only subtraction is defined between packet pointers ({where})" + ) + if not isinstance(op, (ast.Add, ast.Sub)): + raise SyntaxError( + f"only + and - with an offset are allowed on a packet pointer ({where})" + ) + if right_pkt and isinstance(op, ast.Sub): + raise SyntaxError(f"cannot subtract a packet pointer from an offset ({where})") + + ptr, ptr_src_ty, off, off_ty = ( + (left, left_ty, right, right_ty) + if left_pkt + else (right, right_ty, left, left_ty) + ) + if isinstance(off, ir.Constant) and isinstance(off.constant, int): + if not 0 <= off.constant <= MAX_PACKET_OFF: + raise SyntaxError( + f"packet offset {off.constant} is outside 0..{MAX_PACKET_OFF} " + f"({where}); subtract instead of adding a negative offset" + ) + ptr = convert(builder, ptr, ptr_src_ty, ptr_ty) + # The offset as u16, widened with zero-extension: a value the verifier can + # bound to [0, 0xffff]. + off = builder.trunc(off, ir.IntType(16)) if off.type.width > 16 else off + off = builder.zext(off, ir.IntType(64)) if off.type.width < 64 else off + result = builder.add(ptr, off) if isinstance(op, ast.Add) else builder.sub(ptr, off) + return result, ptr_ty + + def _handle_binary_op( func, compilation_context, diff --git a/pythonbpf/expr/packet_pointer.py b/pythonbpf/expr/packet_pointer.py new file mode 100644 index 00000000..aec27696 --- /dev/null +++ b/pythonbpf/expr/packet_pointer.py @@ -0,0 +1,46 @@ +"""Packet-pointer context fields. + +A few context fields are declared u32 in C but are packet pointers to the +verifier: it rewrites the context load into a 64-bit pointer load. C's +integer rules (which rank them as u32) therefore produce code the verifier +rejects ("32-bit pointer arithmetic prohibited"); C programs dodge that by +casting through (void *)(long). The compiler special-cases them instead, so +users never need the cast: + +- the field is used at 64 bits, as the pointer it is; +- an offset added to or subtracted from it is taken as an unsigned 16-bit + value (truncated, then zero-extended), because the verifier only tracks a + packet range whose offset stays within MAX_PACKET_OFF (0xffff); +- pointer - pointer (e.g. data_end - data) is an ordinary 64-bit length. + +Only direct field reads (`ctx.data`) are recognised so far; a local copied +from one (`d = ctx.data`) is an ordinary integer again. Comparisons between +packet pointers are the next site of the same rule. +""" + +import ast + +# Context struct -> fields the verifier treats as packet pointers. Taken from +# the verifier's is_valid_access callbacks, restricted to the fields that +# vmlinux declares as 32-bit integers. +PACKET_POINTER_FIELDS = { + "struct_xdp_md": {"data", "data_end", "data_meta"}, + "struct___sk_buff": {"data", "data_end", "data_meta"}, +} + +MAX_PACKET_OFF = 0xFFFF + + +def is_packet_pointer(expr, local_sym_tab) -> bool: + """True for `ctx.` where ctx is a context parameter of one of the + structs above and the field is one of its packet-pointer fields.""" + if not (isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name)): + return False + sym = (local_sym_tab or {}).get(expr.value.id) + if sym is None: + return False + meta = sym.metadata + struct_name = meta if isinstance(meta, str) else getattr(meta, "__name__", None) + if not isinstance(struct_name, str): + return False + return expr.attr in PACKET_POINTER_FIELDS.get(struct_name, set()) diff --git a/pythonbpf/expr/type_inference.py b/pythonbpf/expr/type_inference.py index 9a0c56b8..a9b42342 100644 --- a/pythonbpf/expr/type_inference.py +++ b/pythonbpf/expr/type_inference.py @@ -21,6 +21,7 @@ ) from .operators import usual_arithmetic_conversions from .vmlinux_registry import VmlinuxHandlerRegistry +from .packet_pointer import is_packet_pointer def _as_intty(ty): @@ -46,6 +47,10 @@ def infer_int_type(expr, local_sym_tab, compilation_context): return None if isinstance(expr, ast.BinOp): + if is_packet_pointer(expr.left, local_sym_tab) or is_packet_pointer( + expr.right, local_sym_tab + ): + return IntTy(64, False) # packet-pointer arithmetic is 64-bit left = infer_int_type(expr.left, local_sym_tab, compilation_context) right = infer_int_type(expr.right, local_sym_tab, compilation_context) if left is None or right is None: diff --git a/tests/failing_tests/vmlinux/ctx_data_arith.py b/tests/failing_tests/vmlinux/ctx_data_arith.py deleted file mode 100644 index d09a5d6d..00000000 --- a/tests/failing_tests/vmlinux/ctx_data_arith.py +++ /dev/null @@ -1,27 +0,0 @@ -# xdp_md.data is declared u32, so C's rules make ctx.data - k a 32-bit -# subtraction, and that is what the compiler emits (w0 += -1). The verifier -# tracks data as a packet pointer and rejects it: "R0 32-bit pointer -# arithmetic prohibited". C hits the same and casts through (void *)(long). -# The planned fix gives the packet-pointer fields 64-bit pointer rank so no -# cast is needed; see the TODO in expr_pass._descriptor. The same u32 rank on -# a scalar field is pinned as passing in passing_tests/vmlinux/ctx_field_rank.py. -from ctypes import c_int32, c_int64 -from pythonbpf import bpf, section, bpfglobal, compile -from vmlinux import struct_xdp_md - - -@bpf -@section("xdp") -def prog(ctx: struct_xdp_md) -> c_int64: - k = c_int32(1) - d = ctx.data - k - return c_int64(d) - - -@bpf -@bpfglobal -def LICENSE() -> str: - return "GPL" - - -compile() diff --git a/tests/failing_tests/vmlinux/ctx_data_offset_too_big.py b/tests/failing_tests/vmlinux/ctx_data_offset_too_big.py new file mode 100644 index 00000000..32dc4a36 --- /dev/null +++ b/tests/failing_tests/vmlinux/ctx_data_offset_too_big.py @@ -0,0 +1,21 @@ +# 70000 cannot be a packet offset: the verifier tracks packet ranges only +# up to 0xffff. Rejected at compile time rather than silently wrapped. +from ctypes import c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + d = ctx.data + 70000 + return c_int64(d) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_arith.py b/tests/passing_tests/vmlinux/ctx_data_arith.py new file mode 100644 index 00000000..3006a41a --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_arith.py @@ -0,0 +1,26 @@ +# xdp_md.data is declared u32, but the verifier treats it as a packet pointer +# and prohibits 32-bit arithmetic on it. C's integer rules would rank +# ctx.data - k as a u32 subtraction (w0 += -1, rejected with "R0 32-bit +# pointer arithmetic prohibited"); the compiler special-cases packet-pointer +# fields instead (pythonbpf/expr/packet_pointer.py): 64-bit arithmetic, the +# offset taken as u16, so this is r0 += -1 and no cast is needed. +from ctypes import c_int32, c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + k = c_int32(1) + d = ctx.data - k + return c_int64(d) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_length.py b/tests/passing_tests/vmlinux/ctx_data_length.py new file mode 100644 index 00000000..6d96ba79 --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_length.py @@ -0,0 +1,21 @@ +# Pointer minus pointer is a length: an ordinary 64-bit subtraction, with no +# 16-bit clamp on either side. +from ctypes import c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + n = ctx.data_end - ctx.data + return c_int64(n) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_plus_one.py b/tests/passing_tests/vmlinux/ctx_data_plus_one.py new file mode 100644 index 00000000..9faa235c --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_plus_one.py @@ -0,0 +1,21 @@ +# Adding a constant to a packet pointer is 64-bit pointer arithmetic +# (r0 += 1), not the u32 addition C's integer rules would give xdp_md.data. +from ctypes import c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + d = ctx.data + 1 + return c_int64(d) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_runtime_offset.py b/tests/passing_tests/vmlinux/ctx_data_runtime_offset.py new file mode 100644 index 00000000..4adde4b2 --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_runtime_offset.py @@ -0,0 +1,24 @@ +# A runtime offset added to a packet pointer is taken as an unsigned 16-bit +# value (masked to 0xffff, zero-extended), so the verifier can bound the +# packet range; the addition itself is 64-bit. +from ctypes import c_int64 +from pythonbpf import bpf, section, bpfglobal, compile +from pythonbpf.helper import random +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + off = random() + d = ctx.data + off + return c_int64(d) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/test_config.toml b/tests/test_config.toml index 3f81509f..3f49730c 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -61,6 +61,6 @@ "failing_tests/loops/for_map_items.py" = {reason = "for/while loops not implemented: no sugar over bpf_for_each_map_elem()-style map iteration exists yet", level = "ir"} -# 32-bit arithmetic on a packet-pointer context field. IR and llc succeed; the -# verifier rejects it with "R0 32-bit pointer arithmetic prohibited". -"failing_tests/vmlinux/ctx_data_arith.py" = {reason = "xdp_md.data is u32 in C but a packet pointer to the verifier; 32-bit arithmetic on it is prohibited until packet-pointer fields get 64-bit pointer rank", level = "verifier"} + +# A packet offset has to fit the verifier's 16-bit packet range. +"failing_tests/vmlinux/ctx_data_offset_too_big.py" = {reason = "A constant packet offset outside 0..65535 is a compile error (the verifier tracks packet ranges up to MAX_PACKET_OFF)", level = "ir"} diff --git a/tests/test_signedness_ir.py b/tests/test_signedness_ir.py index 2166ed88..6bfdca99 100644 --- a/tests/test_signedness_ir.py +++ b/tests/test_signedness_ir.py @@ -68,6 +68,26 @@ [r"\bsub i64", r"trunc i64 .* to i32", r"zext i32 .* to i64"], [], ), + # Packet-pointer fields: 64-bit arithmetic, never the field's u32 rank, + # offsets as u16 (trunc to i16, zext); pointer - pointer is a plain sub. + # No computed value is cut to 32 bits (constant stores such as + # k = c_int32(1) legitimately are). + "vmlinux/ctx_data_arith.py": ( + [r"\bsub i64"], + [r"trunc i64 %.* to i32"], + ), + "vmlinux/ctx_data_plus_one.py": ( + [r"\badd i64"], + [r"trunc i64 %.* to i32"], + ), + "vmlinux/ctx_data_runtime_offset.py": ( + [r"trunc i64 .* to i16", r"zext i16 .* to i64", r"\badd i64"], + [r"trunc i64 %.* to i32"], + ), + "vmlinux/ctx_data_length.py": ( + [r"\bsub i64"], + [r"to i16", r"trunc i64 %.* to i32"], + ), # A bool widens with zext and an integer narrows to it by != 0, never by # trunc; sext of an i1 would return -1 for True. "signedness/bool_int.py": ( From b9b582a7d8def0a7096eebd5c3fde3dddc7ac0da Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 25 Sep 2026 13:03:30 +0530 Subject: [PATCH 3/5] Core: Carry the packet-pointer kind on the type descriptor The previous commit recognised a packet-pointer field only where it was read directly in a binary operation, so d = ctx.data made d an ordinary integer again and comparisons such as data + 34 > data_end were still ranked u32 and emitted as 32-bit compares, which the verifier rejects. PktPtrTy is a 64-bit unsigned IntTy tagged with the verifier's kind (pkt, pkt_meta, pkt_end). It originates at the field read and travels as the descriptor does: into a local's slot, through pointer +/- offset (which yields the same kind), and into comparisons, which compare at 64 bits unsigned whenever either side is a packet pointer. It is handled before the usual arithmetic conversions, which would re-rank it. Two rules the verifier enforces become compile errors: a packet-end pointer takes no offset, and convert() refuses to narrow a packet pointer below 64 bits. Co-Authored-By: Claude Opus 5.5 (1M context) --- pythonbpf/allocation_pass.py | 15 ++++++- pythonbpf/expr/expr_pass.py | 58 +++++++++++++++++----------- pythonbpf/expr/packet_pointer.py | 46 +++++++++++++++------- pythonbpf/expr/type_inference.py | 16 +++++--- pythonbpf/expr/type_normalization.py | 11 +++++- pythonbpf/type_deducer.py | 27 +++++++++++++ 6 files changed, 127 insertions(+), 46 deletions(-) diff --git a/pythonbpf/allocation_pass.py b/pythonbpf/allocation_pass.py index aaf25391..7262cdb8 100644 --- a/pythonbpf/allocation_pass.py +++ b/pythonbpf/allocation_pass.py @@ -6,7 +6,8 @@ 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, IntTy, signedness, PktPtrTy +from pythonbpf.expr.packet_pointer import packet_type from pythonbpf.expr.type_inference import infer_int_type from pythonbpf.maps import BPFMapType @@ -299,7 +300,10 @@ def _allocate_for_binop(builder, var_name, rval, local_sym_tab, compilation_cont inferred = infer_int_type(rval, local_sym_tab, compilation_context) if inferred is None: logger.debug(f"Could not infer a type for {var_name}, assuming signed i64") - ir_type = IntTy(64, signedness(inferred) if inferred is not None else True) + if isinstance(inferred, PktPtrTy): + ir_type = inferred # data + 14 is still a packet pointer + else: + ir_type = IntTy(64, signedness(inferred) if inferred is not None else True) var = builder.alloca(ir_type, name=var_name) var.align = 8 local_sym_tab[var_name] = LocalSymbol(var, ir_type) @@ -399,6 +403,13 @@ def _allocate_for_attribute( # Same discriminator handle_vmlinux_struct_field uses: a context # argument has no alloca of its own. is_context_field = local_sym_tab[struct_var].var is None + pkt_ty = packet_type(rval, local_sym_tab) + if pkt_ty is not None: + # A packet-pointer field: a 64-bit slot that carries the kind. + var = _allocate_with_type(builder, var_name, pkt_ty) + local_sym_tab[var_name] = LocalSymbol(var, pkt_ty) + logger.info(f"Pre-allocated {var_name} as {pkt_ty.describe()}") + return if not VmlinuxHandlerRegistry.has_field(vmlinux_struct_name, field_name): logger.error( f"Field '{field_name}' not found in vmlinux struct '{vmlinux_struct_name}'" diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index b74a4ded..b1d304b5 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -9,6 +9,7 @@ field_int_type, is_ctypes, IntTy, + PktPtrTy, int_literal_type, signedness, ) @@ -24,7 +25,7 @@ get_base_type_and_depth, ) from .vmlinux_registry import VmlinuxHandlerRegistry -from .packet_pointer import MAX_PACKET_OFF, is_packet_pointer +from .packet_pointer import MAX_PACKET_OFF, packet_type from ..vmlinux_parser.dependency_node import Field logger: Logger = logging.getLogger(__name__) @@ -123,6 +124,11 @@ def _handle_attribute_expr( expr, local_sym_tab, None, builder ) if vmlinux_result is not None: + pkt_ty = packet_type(expr, local_sym_tab) + if pkt_ty is not None: + # A packet-pointer field: the value is the loaded + # pointer, and the descriptor says so from here on. + return vmlinux_result[0], pkt_ty return vmlinux_result else: raise RuntimeError("Vmlinux struct did not process successfully") @@ -200,6 +206,8 @@ def _descriptor(val, ty): value unless the descriptor is itself an integer type, sign from the descriptor (an IntTy, a vmlinux Field, or plain -> signed). None when the value is not an integer at all.""" + if isinstance(ty, PktPtrTy): + return ty # a packet pointer keeps its kind, never re-ranked if isinstance(ty, ir.IntType): return IntTy(ty.width, signedness(ty)) field = field_int_type(ty) @@ -285,18 +293,16 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t narrowed to it -- so u32 * u32 wraps at 32 bits even though the arithmetic itself runs in an i64 register. Returns (value, IntTy).""" op = rval.op - left_pkt = is_packet_pointer(rval.left, local_sym_tab) - right_pkt = is_packet_pointer(rval.right, local_sym_tab) left, left_ty = get_typed_operand( func, compilation_context, rval.left, builder, local_sym_tab ) right, right_ty = get_typed_operand( func, compilation_context, rval.right, builder, local_sym_tab ) - if left_pkt or right_pkt: - return _packet_pointer_binop( - builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt - ) + if isinstance(left_ty, PktPtrTy) or isinstance(right_ty, PktPtrTy): + # Before the usual arithmetic conversions, which would re-rank the + # pointer as an ordinary integer and drop its kind. + return _packet_pointer_binop(builder, rval, left, left_ty, right, right_ty) result_ty = usual_arithmetic_conversions(left_ty, right_ty) logger.info( f"binop {type(op).__name__}: {left_ty.describe()} x {right_ty.describe()} " @@ -308,21 +314,20 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t return canonicalise(builder, result, result_ty), result_ty -def _packet_pointer_binop( - builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt -): +def _packet_pointer_binop(builder, rval, left, left_ty, right, right_ty): """A binary operation with a packet-pointer operand (see packet_pointer.py): - 64-bit, never ranked as the field's declared u32, with the offset taken as - an unsigned 16-bit value so the verifier can track the packet range.""" + 64-bit, never ranked as an ordinary integer, with the offset taken as an + unsigned 16-bit value so the verifier can track the packet range. The + result of pointer +/- offset is a pointer of the same kind; pointer - + pointer is a plain 64-bit length.""" op = rval.op where = f"line {rval.lineno}: {ast.unparse(rval)}" - ptr_ty = IntTy(64, False) + left_pkt = isinstance(left_ty, PktPtrTy) + right_pkt = isinstance(right_ty, PktPtrTy) if left_pkt and right_pkt: if isinstance(op, ast.Sub): # data_end - data: a length, an ordinary 64-bit scalar. - left = convert(builder, left, left_ty, ptr_ty) - right = convert(builder, right, right_ty, ptr_ty) - return builder.sub(left, right), ptr_ty + return builder.sub(left, right), IntTy(64, False) raise SyntaxError( f"only subtraction is defined between packet pointers ({where})" ) @@ -333,24 +338,24 @@ def _packet_pointer_binop( if right_pkt and isinstance(op, ast.Sub): raise SyntaxError(f"cannot subtract a packet pointer from an offset ({where})") - ptr, ptr_src_ty, off, off_ty = ( - (left, left_ty, right, right_ty) - if left_pkt - else (right, right_ty, left, left_ty) - ) + ptr, ptr_ty, off = (left, left_ty, right) if left_pkt else (right, right_ty, left) + if ptr_ty.kind == "pkt_end": + # The verifier only compares against the end of the packet. + raise SyntaxError( + f"a packet-end pointer can only be compared, not offset ({where})" + ) if isinstance(off, ir.Constant) and isinstance(off.constant, int): if not 0 <= off.constant <= MAX_PACKET_OFF: raise SyntaxError( f"packet offset {off.constant} is outside 0..{MAX_PACKET_OFF} " f"({where}); subtract instead of adding a negative offset" ) - ptr = convert(builder, ptr, ptr_src_ty, ptr_ty) # The offset as u16, widened with zero-extension: a value the verifier can # bound to [0, 0xffff]. off = builder.trunc(off, ir.IntType(16)) if off.type.width > 16 else off off = builder.zext(off, ir.IntType(64)) if off.type.width < 64 else off result = builder.add(ptr, off) if isinstance(op, ast.Add) else builder.sub(ptr, off) - return result, ptr_ty + return result, PktPtrTy(ptr_ty.kind) def _handle_binary_op( @@ -462,6 +467,13 @@ def _handle_compare(func, compilation_context, builder, cond, local_sym_tab): lhs, lhs_ty = lhs rhs, rhs_ty = rhs lhs_desc, rhs_desc = _descriptor(lhs, lhs_ty), _descriptor(rhs, rhs_ty) + if isinstance(lhs_desc, PktPtrTy) or isinstance(rhs_desc, PktPtrTy): + # A packet bounds check (data + n > data_end): the verifier accepts + # pointer comparisons only at 64 bits, and they are unsigned. + u64 = IntTy(64, False) + lhs = convert(builder, lhs, lhs_desc, u64) + rhs = convert(builder, rhs, rhs_desc, u64) + return handle_comparator(func, builder, cond.ops[0], lhs, rhs, signed=False) if lhs_desc is not None and rhs_desc is not None: # Both integers: compare in the promoted type, which also picks the # signed or unsigned predicate (u64 > s64 is an unsigned compare in C). diff --git a/pythonbpf/expr/packet_pointer.py b/pythonbpf/expr/packet_pointer.py index aec27696..baa05128 100644 --- a/pythonbpf/expr/packet_pointer.py +++ b/pythonbpf/expr/packet_pointer.py @@ -13,34 +13,50 @@ packet range whose offset stays within MAX_PACKET_OFF (0xffff); - pointer - pointer (e.g. data_end - data) is an ordinary 64-bit length. -Only direct field reads (`ctx.data`) are recognised so far; a local copied -from one (`d = ctx.data`) is an ordinary integer again. Comparisons between -packet pointers are the next site of the same rule. +The field read gives its value a PktPtrTy descriptor (a 64-bit unsigned +IntTy tagged with the verifier's kind), and the rules key on that descriptor, +so they follow the pointer through locals (`d = ctx.data`), through +`d + 14`, and into comparisons (`ctx.data + 34 > ctx.data_end` is a 64-bit +unsigned compare). packet-end pointers take no offset at all, and nothing +narrows a packet pointer below 64 bits. """ import ast -# Context struct -> fields the verifier treats as packet pointers. Taken from -# the verifier's is_valid_access callbacks, restricted to the fields that -# vmlinux declares as 32-bit integers. +from pythonbpf.type_deducer import PktPtrTy + +# Context struct -> {field: packet kind}. Taken from the verifier's +# is_valid_access callbacks, restricted to the fields vmlinux declares as +# 32-bit integers. PACKET_POINTER_FIELDS = { - "struct_xdp_md": {"data", "data_end", "data_meta"}, - "struct___sk_buff": {"data", "data_end", "data_meta"}, + "struct_xdp_md": {"data": "pkt", "data_meta": "pkt_meta", "data_end": "pkt_end"}, + "struct___sk_buff": { + "data": "pkt", + "data_meta": "pkt_meta", + "data_end": "pkt_end", + }, } MAX_PACKET_OFF = 0xFFFF -def is_packet_pointer(expr, local_sym_tab) -> bool: - """True for `ctx.` where ctx is a context parameter of one of the - structs above and the field is one of its packet-pointer fields.""" +def packet_kind(expr, local_sym_tab) -> "str | None": + """The packet kind of `ctx.` when ctx is a context parameter of one + of the structs above and the field is one of its packet-pointer fields; + None otherwise. This is the only place a PktPtrTy originates: from here it + travels as a descriptor, through locals, binop results and comparisons.""" if not (isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name)): - return False + return None sym = (local_sym_tab or {}).get(expr.value.id) if sym is None: - return False + return None meta = sym.metadata struct_name = meta if isinstance(meta, str) else getattr(meta, "__name__", None) if not isinstance(struct_name, str): - return False - return expr.attr in PACKET_POINTER_FIELDS.get(struct_name, set()) + return None + return PACKET_POINTER_FIELDS.get(struct_name, {}).get(expr.attr) + + +def packet_type(expr, local_sym_tab) -> "PktPtrTy | None": + kind = packet_kind(expr, local_sym_tab) + return PktPtrTy(kind) if kind is not None else None diff --git a/pythonbpf/expr/type_inference.py b/pythonbpf/expr/type_inference.py index a9b42342..89dcf8df 100644 --- a/pythonbpf/expr/type_inference.py +++ b/pythonbpf/expr/type_inference.py @@ -13,6 +13,7 @@ from llvmlite import ir from pythonbpf.type_deducer import ( + PktPtrTy, IntTy, ctypes_to_ir, is_ctypes, @@ -21,10 +22,12 @@ ) from .operators import usual_arithmetic_conversions from .vmlinux_registry import VmlinuxHandlerRegistry -from .packet_pointer import is_packet_pointer +from .packet_pointer import packet_type def _as_intty(ty): + if isinstance(ty, PktPtrTy): + return ty if isinstance(ty, ir.IntType): return IntTy(ty.width, signedness(ty)) return None @@ -47,12 +50,12 @@ def infer_int_type(expr, local_sym_tab, compilation_context): return None if isinstance(expr, ast.BinOp): - if is_packet_pointer(expr.left, local_sym_tab) or is_packet_pointer( - expr.right, local_sym_tab - ): - return IntTy(64, False) # packet-pointer arithmetic is 64-bit left = infer_int_type(expr.left, local_sym_tab, compilation_context) right = infer_int_type(expr.right, local_sym_tab, compilation_context) + if isinstance(left, PktPtrTy) and isinstance(right, PktPtrTy): + return IntTy(64, False) # pointer - pointer: a length + if isinstance(left, PktPtrTy) or isinstance(right, PktPtrTy): + return left if isinstance(left, PktPtrTy) else right if left is None or right is None: return None return usual_arithmetic_conversions(left, right) @@ -88,6 +91,9 @@ def infer_int_type(expr, local_sym_tab, compilation_context): return None if isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name): + pkt_ty = packet_type(expr, local_sym_tab) + if pkt_ty is not None: + return pkt_ty base = local_sym_tab.get(expr.value.id) if base is None: return None diff --git a/pythonbpf/expr/type_normalization.py b/pythonbpf/expr/type_normalization.py index e89815f8..5c35fd79 100644 --- a/pythonbpf/expr/type_normalization.py +++ b/pythonbpf/expr/type_normalization.py @@ -1,7 +1,7 @@ import logging from llvmlite import ir from .ir_ops import deref_to_depth -from pythonbpf.type_deducer import IntTy, signedness +from pythonbpf.type_deducer import IntTy, PktPtrTy, signedness from .operators import COMPARISON_OPS logger = logging.getLogger(__name__) @@ -52,6 +52,15 @@ def convert(builder, val, from_ty, to_ty): descriptors (see type_deducer.IntTy); the physical width comes from the value itself, which may already be wider than its descriptor says. """ + if ( + isinstance(from_ty, PktPtrTy) + and isinstance(to_ty, ir.IntType) + and to_ty.width < 64 + ): + raise TypeError( + f"a packet pointer ({from_ty.describe()}) cannot be narrowed to " + f"i{to_ty.width}; the verifier only accepts it at 64 bits" + ) if not (isinstance(to_ty, ir.IntType) and isinstance(val.type, ir.IntType)): # Every caller either checks both sides are integers or sits on a path # that only carries integers, so reaching here is a type error that diff --git a/pythonbpf/type_deducer.py b/pythonbpf/type_deducer.py index f6f412c4..f3477f95 100644 --- a/pythonbpf/type_deducer.py +++ b/pythonbpf/type_deducer.py @@ -41,6 +41,33 @@ def describe(self) -> str: return f"{'i' if self.signed else 'u'}{self.width}" +PACKET_KINDS = ("pkt", "pkt_meta", "pkt_end") + + +class PktPtrTy(IntTy): + """A packet pointer: 64-bit, unsigned, and tagged with the verifier's kind + for it (packet data, packet metadata, or packet end). It is an IntTy so + every integer path accepts it; the few sites that must treat it as a + pointer check isinstance(ty, PktPtrTy) before the integer rules apply. + See pythonbpf/expr/packet_pointer.py for where kinds come from and the + rules applied to them.""" + + def __new__(cls, kind: str): + return IntTy.__new__(cls, 64, False) + + def __init__(self, kind: str): + if kind not in PACKET_KINDS: + raise ValueError(f"unknown packet pointer kind {kind!r}") + IntTy.__init__(self, 64, False) + self.kind = kind + + def __getnewargs__(self): # type: ignore[override] + return (self.kind,) + + def describe(self) -> str: + return f"{self.kind}*" + + def int_literal_type(value: int) -> IntTy: """C's type for an integer constant: `int` if the value fits, else `long long`. Literals and enum constants alike; an enum constant is an From db894087a2ff0f7f216007a08436f5845feb631e Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 25 Sep 2026 13:03:32 +0530 Subject: [PATCH 4/5] Tests: Packet pointers through locals, bounds checks, data_meta, and the refusals Co-Authored-By: Claude Opus 5.5 (1M context) --- .../vmlinux/ctx_data_end_offset.py | 21 ++++++++++++++++ .../vmlinux/ctx_data_narrow_store.py | 22 ++++++++++++++++ .../passing_tests/vmlinux/ctx_bounds_check.py | 25 +++++++++++++++++++ .../vmlinux/ctx_bounds_check_direct.py | 21 ++++++++++++++++ .../vmlinux/ctx_data_meta_offset.py | 20 +++++++++++++++ .../vmlinux/ctx_data_via_local.py | 22 ++++++++++++++++ tests/test_config.toml | 3 +++ tests/test_signedness_ir.py | 12 +++++++++ 8 files changed, 146 insertions(+) create mode 100644 tests/failing_tests/vmlinux/ctx_data_end_offset.py create mode 100644 tests/failing_tests/vmlinux/ctx_data_narrow_store.py create mode 100644 tests/passing_tests/vmlinux/ctx_bounds_check.py create mode 100644 tests/passing_tests/vmlinux/ctx_bounds_check_direct.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_meta_offset.py create mode 100644 tests/passing_tests/vmlinux/ctx_data_via_local.py diff --git a/tests/failing_tests/vmlinux/ctx_data_end_offset.py b/tests/failing_tests/vmlinux/ctx_data_end_offset.py new file mode 100644 index 00000000..238fe577 --- /dev/null +++ b/tests/failing_tests/vmlinux/ctx_data_end_offset.py @@ -0,0 +1,21 @@ +# data_end can only be compared against; the verifier rejects arithmetic on +# it, so the compiler refuses it. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + e = ctx.data_end + 1 + return c_int64(e) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/vmlinux/ctx_data_narrow_store.py b/tests/failing_tests/vmlinux/ctx_data_narrow_store.py new file mode 100644 index 00000000..984ae76a --- /dev/null +++ b/tests/failing_tests/vmlinux/ctx_data_narrow_store.py @@ -0,0 +1,22 @@ +# A packet pointer stored into a 32-bit slot would lose it; the verifier only +# accepts packet pointers at 64 bits, so the compiler refuses the narrowing. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + x = c_uint32(0) + x = ctx.data + return c_int64(x) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_bounds_check.py b/tests/passing_tests/vmlinux/ctx_bounds_check.py new file mode 100644 index 00000000..4e91357e --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_bounds_check.py @@ -0,0 +1,25 @@ +# The canonical XDP bounds check, through locals: the packet-pointer type +# travels with data and data_end, so data + 34 > data_end is a 64-bit +# unsigned comparison, which is what the verifier needs to prove the range. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + data = ctx.data + data_end = ctx.data_end + if data + 34 > data_end: + return c_int64(1) + return c_int64(2) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_bounds_check_direct.py b/tests/passing_tests/vmlinux/ctx_bounds_check_direct.py new file mode 100644 index 00000000..cac2cbd2 --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_bounds_check_direct.py @@ -0,0 +1,21 @@ +# The same bounds check written on the fields directly. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + if ctx.data + 14 > ctx.data_end: + return c_int64(1) + return c_int64(2) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_meta_offset.py b/tests/passing_tests/vmlinux/ctx_data_meta_offset.py new file mode 100644 index 00000000..05b7c272 --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_meta_offset.py @@ -0,0 +1,20 @@ +# data_meta is a packet-metadata pointer; offsets are allowed on it too. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + m = ctx.data_meta + 4 + return c_int64(m) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/vmlinux/ctx_data_via_local.py b/tests/passing_tests/vmlinux/ctx_data_via_local.py new file mode 100644 index 00000000..1e2d36d7 --- /dev/null +++ b/tests/passing_tests/vmlinux/ctx_data_via_local.py @@ -0,0 +1,22 @@ +# A local copied from a packet-pointer field is still a packet pointer: its +# slot is 64-bit and d + 14 is 64-bit pointer arithmetic. +from ctypes import c_int64, c_uint32 # noqa: F401 +from pythonbpf import bpf, section, bpfglobal, compile +from vmlinux import struct_xdp_md + + +@bpf +@section("xdp") +def prog(ctx: struct_xdp_md) -> c_int64: + data = ctx.data + eth = data + 14 + return c_int64(eth) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/test_config.toml b/tests/test_config.toml index 3f49730c..2e71808b 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -64,3 +64,6 @@ # A packet offset has to fit the verifier's 16-bit packet range. "failing_tests/vmlinux/ctx_data_offset_too_big.py" = {reason = "A constant packet offset outside 0..65535 is a compile error (the verifier tracks packet ranges up to MAX_PACKET_OFF)", level = "ir"} +"failing_tests/vmlinux/ctx_data_end_offset.py" = {reason = "A packet-end pointer can only be compared, not offset (the verifier rejects arithmetic on it)", level = "ir"} + +"failing_tests/vmlinux/ctx_data_narrow_store.py" = {reason = "A packet pointer cannot be narrowed below 64 bits", level = "ir"} diff --git a/tests/test_signedness_ir.py b/tests/test_signedness_ir.py index 6bfdca99..6b8ab714 100644 --- a/tests/test_signedness_ir.py +++ b/tests/test_signedness_ir.py @@ -88,6 +88,18 @@ [r"\bsub i64"], [r"to i16", r"trunc i64 %.* to i32"], ), + "vmlinux/ctx_bounds_check.py": ( + [r"icmp ugt i64", r"\badd i64"], + [r"trunc i64 %.* to i32", r"icmp \w+ i32"], + ), + "vmlinux/ctx_bounds_check_direct.py": ( + [r"icmp ugt i64"], + [r"trunc i64 %.* to i32", r"icmp \w+ i32"], + ), + "vmlinux/ctx_data_via_local.py": ( + [r"\badd i64"], + [r"trunc i64 %.* to i32"], + ), # A bool widens with zext and an integer narrows to it by != 0, never by # trunc; sext of an i1 would return -1 for True. "signedness/bool_int.py": ( From aa612a415176a05726d077d033a1ea50084806b9 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 25 Sep 2026 23:21:12 +0530 Subject: [PATCH 5/5] Core: Let a packet pointer be narrowed on a store, as C does convert() refused to narrow a packet pointer below 64 bits, on the premise that the verifier only accepts one at 64 bits. That is true of arithmetic, not of stores: upstream's cgroup_skb_direct_packet_access stores skb->data_end into a __u32 global, and CI's verifier level accepted exactly that on #105. Merging master brought that selftest in and the refusal rejected it. Narrowing now truncates, and the result is an ordinary integer. ctx_data_narrow_store.py moves to the passing tests. Keeping the pointer when a local is declared narrower belongs to allocation (widest type ever stored), noted for later. Co-Authored-By: Claude Opus 5.5 (1M context) --- pythonbpf/expr/packet_pointer.py | 10 ++++++++-- pythonbpf/expr/type_normalization.py | 11 +---------- .../vmlinux/ctx_data_narrow_store.py | 7 +++++-- tests/test_config.toml | 1 - 4 files changed, 14 insertions(+), 15 deletions(-) rename tests/{failing_tests => passing_tests}/vmlinux/ctx_data_narrow_store.py (51%) diff --git a/pythonbpf/expr/packet_pointer.py b/pythonbpf/expr/packet_pointer.py index baa05128..fe0fa2ff 100644 --- a/pythonbpf/expr/packet_pointer.py +++ b/pythonbpf/expr/packet_pointer.py @@ -17,8 +17,14 @@ IntTy tagged with the verifier's kind), and the rules key on that descriptor, so they follow the pointer through locals (`d = ctx.data`), through `d + 14`, and into comparisons (`ctx.data + 34 > ctx.data_end` is a 64-bit -unsigned compare). packet-end pointers take no offset at all, and nothing -narrows a packet pointer below 64 bits. +unsigned compare). packet-end pointers take no offset at all. + +Storing a packet pointer into a narrower slot truncates it, as C does, and the +result is an ordinary integer: the verifier forbids 32-bit *arithmetic* on a +packet pointer, not a 32-bit store of one (upstream's +cgroup_skb_direct_packet_access stores data_end into a __u32 global). Keeping +the pointer when a local is declared narrower is a job for allocation, which +could give such a slot the widest type ever stored into it. """ import ast diff --git a/pythonbpf/expr/type_normalization.py b/pythonbpf/expr/type_normalization.py index 5c35fd79..e89815f8 100644 --- a/pythonbpf/expr/type_normalization.py +++ b/pythonbpf/expr/type_normalization.py @@ -1,7 +1,7 @@ import logging from llvmlite import ir from .ir_ops import deref_to_depth -from pythonbpf.type_deducer import IntTy, PktPtrTy, signedness +from pythonbpf.type_deducer import IntTy, signedness from .operators import COMPARISON_OPS logger = logging.getLogger(__name__) @@ -52,15 +52,6 @@ def convert(builder, val, from_ty, to_ty): descriptors (see type_deducer.IntTy); the physical width comes from the value itself, which may already be wider than its descriptor says. """ - if ( - isinstance(from_ty, PktPtrTy) - and isinstance(to_ty, ir.IntType) - and to_ty.width < 64 - ): - raise TypeError( - f"a packet pointer ({from_ty.describe()}) cannot be narrowed to " - f"i{to_ty.width}; the verifier only accepts it at 64 bits" - ) if not (isinstance(to_ty, ir.IntType) and isinstance(val.type, ir.IntType)): # Every caller either checks both sides are integers or sits on a path # that only carries integers, so reaching here is a type error that diff --git a/tests/failing_tests/vmlinux/ctx_data_narrow_store.py b/tests/passing_tests/vmlinux/ctx_data_narrow_store.py similarity index 51% rename from tests/failing_tests/vmlinux/ctx_data_narrow_store.py rename to tests/passing_tests/vmlinux/ctx_data_narrow_store.py index 984ae76a..ef7c6ae8 100644 --- a/tests/failing_tests/vmlinux/ctx_data_narrow_store.py +++ b/tests/passing_tests/vmlinux/ctx_data_narrow_store.py @@ -1,5 +1,8 @@ -# A packet pointer stored into a 32-bit slot would lose it; the verifier only -# accepts packet pointers at 64 bits, so the compiler refuses the narrowing. +# A packet pointer stored into a 32-bit local is truncated, as in C +# (`__u32 x = ctx->data`), and is an ordinary integer from then on. The +# verifier forbids 32-bit arithmetic on a packet pointer, not a narrow store +# of one; upstream's cgroup_skb_direct_packet_access does the same with a +# __u32 global. from ctypes import c_int64, c_uint32 # noqa: F401 from pythonbpf import bpf, section, bpfglobal, compile from vmlinux import struct_xdp_md diff --git a/tests/test_config.toml b/tests/test_config.toml index 3d5e9b67..eb680d65 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -68,7 +68,6 @@ "failing_tests/vmlinux/ctx_data_offset_too_big.py" = {reason = "A constant packet offset outside 0..65535 is a compile error (the verifier tracks packet ranges up to MAX_PACKET_OFF)", level = "ir"} "failing_tests/vmlinux/ctx_data_end_offset.py" = {reason = "A packet-end pointer can only be compared, not offset (the verifier rejects arithmetic on it)", level = "ir"} -"failing_tests/vmlinux/ctx_data_narrow_store.py" = {reason = "A packet pointer cannot be narrowed below 64 bits", level = "ir"} "kernel_selftest_equivalent/ringbuf/reserve_submit_discard.py" = {reason = "RingBuffer reserve/typed record/discard workflow is planned but not implemented yet", level = "ir"}