Skip to content

Commit b9b582a

Browse files
r41k0uclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 6a859ac commit b9b582a

6 files changed

Lines changed: 127 additions & 46 deletions

File tree

‎pythonbpf/allocation_pass.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
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, IntTy, signedness, PktPtrTy
10+
from pythonbpf.expr.packet_pointer import packet_type
1011
from pythonbpf.expr.type_inference import infer_int_type
1112
from pythonbpf.maps import BPFMapType
1213

@@ -299,7 +300,10 @@ def _allocate_for_binop(builder, var_name, rval, local_sym_tab, compilation_cont
299300
inferred = infer_int_type(rval, local_sym_tab, compilation_context)
300301
if inferred is None:
301302
logger.debug(f"Could not infer a type for {var_name}, assuming signed i64")
302-
ir_type = IntTy(64, signedness(inferred) if inferred is not None else True)
303+
if isinstance(inferred, PktPtrTy):
304+
ir_type = inferred # data + 14 is still a packet pointer
305+
else:
306+
ir_type = IntTy(64, signedness(inferred) if inferred is not None else True)
303307
var = builder.alloca(ir_type, name=var_name)
304308
var.align = 8
305309
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
@@ -399,6 +403,13 @@ def _allocate_for_attribute(
399403
# Same discriminator handle_vmlinux_struct_field uses: a context
400404
# argument has no alloca of its own.
401405
is_context_field = local_sym_tab[struct_var].var is None
406+
pkt_ty = packet_type(rval, local_sym_tab)
407+
if pkt_ty is not None:
408+
# A packet-pointer field: a 64-bit slot that carries the kind.
409+
var = _allocate_with_type(builder, var_name, pkt_ty)
410+
local_sym_tab[var_name] = LocalSymbol(var, pkt_ty)
411+
logger.info(f"Pre-allocated {var_name} as {pkt_ty.describe()}")
412+
return
402413
if not VmlinuxHandlerRegistry.has_field(vmlinux_struct_name, field_name):
403414
logger.error(
404415
f"Field '{field_name}' not found in vmlinux struct '{vmlinux_struct_name}'"

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
field_int_type,
1010
is_ctypes,
1111
IntTy,
12+
PktPtrTy,
1213
int_literal_type,
1314
signedness,
1415
)
@@ -24,7 +25,7 @@
2425
get_base_type_and_depth,
2526
)
2627
from .vmlinux_registry import VmlinuxHandlerRegistry
27-
from .packet_pointer import MAX_PACKET_OFF, is_packet_pointer
28+
from .packet_pointer import MAX_PACKET_OFF, packet_type
2829
from ..vmlinux_parser.dependency_node import Field
2930

3031
logger: Logger = logging.getLogger(__name__)
@@ -123,6 +124,11 @@ def _handle_attribute_expr(
123124
expr, local_sym_tab, None, builder
124125
)
125126
if vmlinux_result is not None:
127+
pkt_ty = packet_type(expr, local_sym_tab)
128+
if pkt_ty is not None:
129+
# A packet-pointer field: the value is the loaded
130+
# pointer, and the descriptor says so from here on.
131+
return vmlinux_result[0], pkt_ty
126132
return vmlinux_result
127133
else:
128134
raise RuntimeError("Vmlinux struct did not process successfully")
@@ -200,6 +206,8 @@ def _descriptor(val, ty):
200206
value unless the descriptor is itself an integer type, sign from the
201207
descriptor (an IntTy, a vmlinux Field, or plain -> signed). None when the
202208
value is not an integer at all."""
209+
if isinstance(ty, PktPtrTy):
210+
return ty # a packet pointer keeps its kind, never re-ranked
203211
if isinstance(ty, ir.IntType):
204212
return IntTy(ty.width, signedness(ty))
205213
field = field_int_type(ty)
@@ -285,18 +293,16 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t
285293
narrowed to it -- so u32 * u32 wraps at 32 bits even though the arithmetic
286294
itself runs in an i64 register. Returns (value, IntTy)."""
287295
op = rval.op
288-
left_pkt = is_packet_pointer(rval.left, local_sym_tab)
289-
right_pkt = is_packet_pointer(rval.right, local_sym_tab)
290296
left, left_ty = get_typed_operand(
291297
func, compilation_context, rval.left, builder, local_sym_tab
292298
)
293299
right, right_ty = get_typed_operand(
294300
func, compilation_context, rval.right, builder, local_sym_tab
295301
)
296-
if left_pkt or right_pkt:
297-
return _packet_pointer_binop(
298-
builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt
299-
)
302+
if isinstance(left_ty, PktPtrTy) or isinstance(right_ty, PktPtrTy):
303+
# Before the usual arithmetic conversions, which would re-rank the
304+
# pointer as an ordinary integer and drop its kind.
305+
return _packet_pointer_binop(builder, rval, left, left_ty, right, right_ty)
300306
result_ty = usual_arithmetic_conversions(left_ty, right_ty)
301307
logger.info(
302308
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
308314
return canonicalise(builder, result, result_ty), result_ty
309315

310316

311-
def _packet_pointer_binop(
312-
builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt
313-
):
317+
def _packet_pointer_binop(builder, rval, left, left_ty, right, right_ty):
314318
"""A binary operation with a packet-pointer operand (see packet_pointer.py):
315-
64-bit, never ranked as the field's declared u32, with the offset taken as
316-
an unsigned 16-bit value so the verifier can track the packet range."""
319+
64-bit, never ranked as an ordinary integer, with the offset taken as an
320+
unsigned 16-bit value so the verifier can track the packet range. The
321+
result of pointer +/- offset is a pointer of the same kind; pointer -
322+
pointer is a plain 64-bit length."""
317323
op = rval.op
318324
where = f"line {rval.lineno}: {ast.unparse(rval)}"
319-
ptr_ty = IntTy(64, False)
325+
left_pkt = isinstance(left_ty, PktPtrTy)
326+
right_pkt = isinstance(right_ty, PktPtrTy)
320327
if left_pkt and right_pkt:
321328
if isinstance(op, ast.Sub):
322329
# data_end - data: a length, an ordinary 64-bit scalar.
323-
left = convert(builder, left, left_ty, ptr_ty)
324-
right = convert(builder, right, right_ty, ptr_ty)
325-
return builder.sub(left, right), ptr_ty
330+
return builder.sub(left, right), IntTy(64, False)
326331
raise SyntaxError(
327332
f"only subtraction is defined between packet pointers ({where})"
328333
)
@@ -333,24 +338,24 @@ def _packet_pointer_binop(
333338
if right_pkt and isinstance(op, ast.Sub):
334339
raise SyntaxError(f"cannot subtract a packet pointer from an offset ({where})")
335340

336-
ptr, ptr_src_ty, off, off_ty = (
337-
(left, left_ty, right, right_ty)
338-
if left_pkt
339-
else (right, right_ty, left, left_ty)
340-
)
341+
ptr, ptr_ty, off = (left, left_ty, right) if left_pkt else (right, right_ty, left)
342+
if ptr_ty.kind == "pkt_end":
343+
# The verifier only compares against the end of the packet.
344+
raise SyntaxError(
345+
f"a packet-end pointer can only be compared, not offset ({where})"
346+
)
341347
if isinstance(off, ir.Constant) and isinstance(off.constant, int):
342348
if not 0 <= off.constant <= MAX_PACKET_OFF:
343349
raise SyntaxError(
344350
f"packet offset {off.constant} is outside 0..{MAX_PACKET_OFF} "
345351
f"({where}); subtract instead of adding a negative offset"
346352
)
347-
ptr = convert(builder, ptr, ptr_src_ty, ptr_ty)
348353
# The offset as u16, widened with zero-extension: a value the verifier can
349354
# bound to [0, 0xffff].
350355
off = builder.trunc(off, ir.IntType(16)) if off.type.width > 16 else off
351356
off = builder.zext(off, ir.IntType(64)) if off.type.width < 64 else off
352357
result = builder.add(ptr, off) if isinstance(op, ast.Add) else builder.sub(ptr, off)
353-
return result, ptr_ty
358+
return result, PktPtrTy(ptr_ty.kind)
354359

355360

356361
def _handle_binary_op(
@@ -462,6 +467,13 @@ def _handle_compare(func, compilation_context, builder, cond, local_sym_tab):
462467
lhs, lhs_ty = lhs
463468
rhs, rhs_ty = rhs
464469
lhs_desc, rhs_desc = _descriptor(lhs, lhs_ty), _descriptor(rhs, rhs_ty)
470+
if isinstance(lhs_desc, PktPtrTy) or isinstance(rhs_desc, PktPtrTy):
471+
# A packet bounds check (data + n > data_end): the verifier accepts
472+
# pointer comparisons only at 64 bits, and they are unsigned.
473+
u64 = IntTy(64, False)
474+
lhs = convert(builder, lhs, lhs_desc, u64)
475+
rhs = convert(builder, rhs, rhs_desc, u64)
476+
return handle_comparator(func, builder, cond.ops[0], lhs, rhs, signed=False)
465477
if lhs_desc is not None and rhs_desc is not None:
466478
# Both integers: compare in the promoted type, which also picks the
467479
# signed or unsigned predicate (u64 > s64 is an unsigned compare in C).

‎pythonbpf/expr/packet_pointer.py‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,50 @@
1313
packet range whose offset stays within MAX_PACKET_OFF (0xffff);
1414
- pointer - pointer (e.g. data_end - data) is an ordinary 64-bit length.
1515
16-
Only direct field reads (`ctx.data`) are recognised so far; a local copied
17-
from one (`d = ctx.data`) is an ordinary integer again. Comparisons between
18-
packet pointers are the next site of the same rule.
16+
The field read gives its value a PktPtrTy descriptor (a 64-bit unsigned
17+
IntTy tagged with the verifier's kind), and the rules key on that descriptor,
18+
so they follow the pointer through locals (`d = ctx.data`), through
19+
`d + 14`, and into comparisons (`ctx.data + 34 > ctx.data_end` is a 64-bit
20+
unsigned compare). packet-end pointers take no offset at all, and nothing
21+
narrows a packet pointer below 64 bits.
1922
"""
2023

2124
import ast
2225

23-
# Context struct -> fields the verifier treats as packet pointers. Taken from
24-
# the verifier's is_valid_access callbacks, restricted to the fields that
25-
# vmlinux declares as 32-bit integers.
26+
from pythonbpf.type_deducer import PktPtrTy
27+
28+
# Context struct -> {field: packet kind}. Taken from the verifier's
29+
# is_valid_access callbacks, restricted to the fields vmlinux declares as
30+
# 32-bit integers.
2631
PACKET_POINTER_FIELDS = {
27-
"struct_xdp_md": {"data", "data_end", "data_meta"},
28-
"struct___sk_buff": {"data", "data_end", "data_meta"},
32+
"struct_xdp_md": {"data": "pkt", "data_meta": "pkt_meta", "data_end": "pkt_end"},
33+
"struct___sk_buff": {
34+
"data": "pkt",
35+
"data_meta": "pkt_meta",
36+
"data_end": "pkt_end",
37+
},
2938
}
3039

3140
MAX_PACKET_OFF = 0xFFFF
3241

3342

34-
def is_packet_pointer(expr, local_sym_tab) -> bool:
35-
"""True for `ctx.<field>` where ctx is a context parameter of one of the
36-
structs above and the field is one of its packet-pointer fields."""
43+
def packet_kind(expr, local_sym_tab) -> "str | None":
44+
"""The packet kind of `ctx.<field>` when ctx is a context parameter of one
45+
of the structs above and the field is one of its packet-pointer fields;
46+
None otherwise. This is the only place a PktPtrTy originates: from here it
47+
travels as a descriptor, through locals, binop results and comparisons."""
3748
if not (isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name)):
38-
return False
49+
return None
3950
sym = (local_sym_tab or {}).get(expr.value.id)
4051
if sym is None:
41-
return False
52+
return None
4253
meta = sym.metadata
4354
struct_name = meta if isinstance(meta, str) else getattr(meta, "__name__", None)
4455
if not isinstance(struct_name, str):
45-
return False
46-
return expr.attr in PACKET_POINTER_FIELDS.get(struct_name, set())
56+
return None
57+
return PACKET_POINTER_FIELDS.get(struct_name, {}).get(expr.attr)
58+
59+
60+
def packet_type(expr, local_sym_tab) -> "PktPtrTy | None":
61+
kind = packet_kind(expr, local_sym_tab)
62+
return PktPtrTy(kind) if kind is not None else None

‎pythonbpf/expr/type_inference.py‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from llvmlite import ir
1414

1515
from pythonbpf.type_deducer import (
16+
PktPtrTy,
1617
IntTy,
1718
ctypes_to_ir,
1819
is_ctypes,
@@ -21,10 +22,12 @@
2122
)
2223
from .operators import usual_arithmetic_conversions
2324
from .vmlinux_registry import VmlinuxHandlerRegistry
24-
from .packet_pointer import is_packet_pointer
25+
from .packet_pointer import packet_type
2526

2627

2728
def _as_intty(ty):
29+
if isinstance(ty, PktPtrTy):
30+
return ty
2831
if isinstance(ty, ir.IntType):
2932
return IntTy(ty.width, signedness(ty))
3033
return None
@@ -47,12 +50,12 @@ def infer_int_type(expr, local_sym_tab, compilation_context):
4750
return None
4851

4952
if isinstance(expr, ast.BinOp):
50-
if is_packet_pointer(expr.left, local_sym_tab) or is_packet_pointer(
51-
expr.right, local_sym_tab
52-
):
53-
return IntTy(64, False) # packet-pointer arithmetic is 64-bit
5453
left = infer_int_type(expr.left, local_sym_tab, compilation_context)
5554
right = infer_int_type(expr.right, local_sym_tab, compilation_context)
55+
if isinstance(left, PktPtrTy) and isinstance(right, PktPtrTy):
56+
return IntTy(64, False) # pointer - pointer: a length
57+
if isinstance(left, PktPtrTy) or isinstance(right, PktPtrTy):
58+
return left if isinstance(left, PktPtrTy) else right
5659
if left is None or right is None:
5760
return None
5861
return usual_arithmetic_conversions(left, right)
@@ -88,6 +91,9 @@ def infer_int_type(expr, local_sym_tab, compilation_context):
8891
return None
8992

9093
if isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name):
94+
pkt_ty = packet_type(expr, local_sym_tab)
95+
if pkt_ty is not None:
96+
return pkt_ty
9197
base = local_sym_tab.get(expr.value.id)
9298
if base is None:
9399
return None

‎pythonbpf/expr/type_normalization.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from llvmlite import ir
33
from .ir_ops import deref_to_depth
4-
from pythonbpf.type_deducer import IntTy, signedness
4+
from pythonbpf.type_deducer import IntTy, PktPtrTy, signedness
55
from .operators import COMPARISON_OPS
66

77
logger = logging.getLogger(__name__)
@@ -52,6 +52,15 @@ def convert(builder, val, from_ty, to_ty):
5252
descriptors (see type_deducer.IntTy); the physical width comes from the
5353
value itself, which may already be wider than its descriptor says.
5454
"""
55+
if (
56+
isinstance(from_ty, PktPtrTy)
57+
and isinstance(to_ty, ir.IntType)
58+
and to_ty.width < 64
59+
):
60+
raise TypeError(
61+
f"a packet pointer ({from_ty.describe()}) cannot be narrowed to "
62+
f"i{to_ty.width}; the verifier only accepts it at 64 bits"
63+
)
5564
if not (isinstance(to_ty, ir.IntType) and isinstance(val.type, ir.IntType)):
5665
# Every caller either checks both sides are integers or sits on a path
5766
# that only carries integers, so reaching here is a type error that

‎pythonbpf/type_deducer.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,33 @@ def describe(self) -> str:
4141
return f"{'i' if self.signed else 'u'}{self.width}"
4242

4343

44+
PACKET_KINDS = ("pkt", "pkt_meta", "pkt_end")
45+
46+
47+
class PktPtrTy(IntTy):
48+
"""A packet pointer: 64-bit, unsigned, and tagged with the verifier's kind
49+
for it (packet data, packet metadata, or packet end). It is an IntTy so
50+
every integer path accepts it; the few sites that must treat it as a
51+
pointer check isinstance(ty, PktPtrTy) before the integer rules apply.
52+
See pythonbpf/expr/packet_pointer.py for where kinds come from and the
53+
rules applied to them."""
54+
55+
def __new__(cls, kind: str):
56+
return IntTy.__new__(cls, 64, False)
57+
58+
def __init__(self, kind: str):
59+
if kind not in PACKET_KINDS:
60+
raise ValueError(f"unknown packet pointer kind {kind!r}")
61+
IntTy.__init__(self, 64, False)
62+
self.kind = kind
63+
64+
def __getnewargs__(self): # type: ignore[override]
65+
return (self.kind,)
66+
67+
def describe(self) -> str:
68+
return f"{self.kind}*"
69+
70+
4471
def int_literal_type(value: int) -> IntTy:
4572
"""C's type for an integer constant: `int` if the value fits, else
4673
`long long`. Literals and enum constants alike; an enum constant is an

0 commit comments

Comments
 (0)