Skip to content

Commit 6a859ac

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

11 files changed

Lines changed: 239 additions & 30 deletions

File tree

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
get_base_type_and_depth,
2525
)
2626
from .vmlinux_registry import VmlinuxHandlerRegistry
27+
from .packet_pointer import MAX_PACKET_OFF, is_packet_pointer
2728
from ..vmlinux_parser.dependency_node import Field
2829

2930
logger: Logger = logging.getLogger(__name__)
@@ -284,12 +285,18 @@ def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_t
284285
narrowed to it -- so u32 * u32 wraps at 32 bits even though the arithmetic
285286
itself runs in an i64 register. Returns (value, IntTy)."""
286287
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)
287290
left, left_ty = get_typed_operand(
288291
func, compilation_context, rval.left, builder, local_sym_tab
289292
)
290293
right, right_ty = get_typed_operand(
291294
func, compilation_context, rval.right, builder, local_sym_tab
292295
)
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+
)
293300
result_ty = usual_arithmetic_conversions(left_ty, right_ty)
294301
logger.info(
295302
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
301308
return canonicalise(builder, result, result_ty), result_ty
302309

303310

311+
def _packet_pointer_binop(
312+
builder, rval, left, left_ty, left_pkt, right, right_ty, right_pkt
313+
):
314+
"""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."""
317+
op = rval.op
318+
where = f"line {rval.lineno}: {ast.unparse(rval)}"
319+
ptr_ty = IntTy(64, False)
320+
if left_pkt and right_pkt:
321+
if isinstance(op, ast.Sub):
322+
# 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
326+
raise SyntaxError(
327+
f"only subtraction is defined between packet pointers ({where})"
328+
)
329+
if not isinstance(op, (ast.Add, ast.Sub)):
330+
raise SyntaxError(
331+
f"only + and - with an offset are allowed on a packet pointer ({where})"
332+
)
333+
if right_pkt and isinstance(op, ast.Sub):
334+
raise SyntaxError(f"cannot subtract a packet pointer from an offset ({where})")
335+
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+
if isinstance(off, ir.Constant) and isinstance(off.constant, int):
342+
if not 0 <= off.constant <= MAX_PACKET_OFF:
343+
raise SyntaxError(
344+
f"packet offset {off.constant} is outside 0..{MAX_PACKET_OFF} "
345+
f"({where}); subtract instead of adding a negative offset"
346+
)
347+
ptr = convert(builder, ptr, ptr_src_ty, ptr_ty)
348+
# The offset as u16, widened with zero-extension: a value the verifier can
349+
# bound to [0, 0xffff].
350+
off = builder.trunc(off, ir.IntType(16)) if off.type.width > 16 else off
351+
off = builder.zext(off, ir.IntType(64)) if off.type.width < 64 else off
352+
result = builder.add(ptr, off) if isinstance(op, ast.Add) else builder.sub(ptr, off)
353+
return result, ptr_ty
354+
355+
304356
def _handle_binary_op(
305357
func,
306358
compilation_context,

‎pythonbpf/expr/packet_pointer.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Packet-pointer context fields.
2+
3+
A few context fields are declared u32 in C but are packet pointers to the
4+
verifier: it rewrites the context load into a 64-bit pointer load. C's
5+
integer rules (which rank them as u32) therefore produce code the verifier
6+
rejects ("32-bit pointer arithmetic prohibited"); C programs dodge that by
7+
casting through (void *)(long). The compiler special-cases them instead, so
8+
users never need the cast:
9+
10+
- the field is used at 64 bits, as the pointer it is;
11+
- an offset added to or subtracted from it is taken as an unsigned 16-bit
12+
value (truncated, then zero-extended), because the verifier only tracks a
13+
packet range whose offset stays within MAX_PACKET_OFF (0xffff);
14+
- pointer - pointer (e.g. data_end - data) is an ordinary 64-bit length.
15+
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.
19+
"""
20+
21+
import ast
22+
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+
PACKET_POINTER_FIELDS = {
27+
"struct_xdp_md": {"data", "data_end", "data_meta"},
28+
"struct___sk_buff": {"data", "data_end", "data_meta"},
29+
}
30+
31+
MAX_PACKET_OFF = 0xFFFF
32+
33+
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."""
37+
if not (isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name)):
38+
return False
39+
sym = (local_sym_tab or {}).get(expr.value.id)
40+
if sym is None:
41+
return False
42+
meta = sym.metadata
43+
struct_name = meta if isinstance(meta, str) else getattr(meta, "__name__", None)
44+
if not isinstance(struct_name, str):
45+
return False
46+
return expr.attr in PACKET_POINTER_FIELDS.get(struct_name, set())

‎pythonbpf/expr/type_inference.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from .operators import usual_arithmetic_conversions
2323
from .vmlinux_registry import VmlinuxHandlerRegistry
24+
from .packet_pointer import is_packet_pointer
2425

2526

2627
def _as_intty(ty):
@@ -46,6 +47,10 @@ def infer_int_type(expr, local_sym_tab, compilation_context):
4647
return None
4748

4849
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
4954
left = infer_int_type(expr.left, local_sym_tab, compilation_context)
5055
right = infer_int_type(expr.right, local_sym_tab, compilation_context)
5156
if left is None or right is None:

‎tests/failing_tests/vmlinux/ctx_data_arith.py‎

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# 70000 cannot be a packet offset: the verifier tracks packet ranges only
2+
# up to 0xffff. Rejected at compile time rather than silently wrapped.
3+
from ctypes import c_int64
4+
from pythonbpf import bpf, section, bpfglobal, compile
5+
from vmlinux import struct_xdp_md
6+
7+
8+
@bpf
9+
@section("xdp")
10+
def prog(ctx: struct_xdp_md) -> c_int64:
11+
d = ctx.data + 70000
12+
return c_int64(d)
13+
14+
15+
@bpf
16+
@bpfglobal
17+
def LICENSE() -> str:
18+
return "GPL"
19+
20+
21+
compile()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# xdp_md.data is declared u32, but the verifier treats it as a packet pointer
2+
# and prohibits 32-bit arithmetic on it. C's integer rules would rank
3+
# ctx.data - k as a u32 subtraction (w0 += -1, rejected with "R0 32-bit
4+
# pointer arithmetic prohibited"); the compiler special-cases packet-pointer
5+
# fields instead (pythonbpf/expr/packet_pointer.py): 64-bit arithmetic, the
6+
# offset taken as u16, so this is r0 += -1 and no cast is needed.
7+
from ctypes import c_int32, c_int64
8+
from pythonbpf import bpf, section, bpfglobal, compile
9+
from vmlinux import struct_xdp_md
10+
11+
12+
@bpf
13+
@section("xdp")
14+
def prog(ctx: struct_xdp_md) -> c_int64:
15+
k = c_int32(1)
16+
d = ctx.data - k
17+
return c_int64(d)
18+
19+
20+
@bpf
21+
@bpfglobal
22+
def LICENSE() -> str:
23+
return "GPL"
24+
25+
26+
compile()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Pointer minus pointer is a length: an ordinary 64-bit subtraction, with no
2+
# 16-bit clamp on either side.
3+
from ctypes import c_int64
4+
from pythonbpf import bpf, section, bpfglobal, compile
5+
from vmlinux import struct_xdp_md
6+
7+
8+
@bpf
9+
@section("xdp")
10+
def prog(ctx: struct_xdp_md) -> c_int64:
11+
n = ctx.data_end - ctx.data
12+
return c_int64(n)
13+
14+
15+
@bpf
16+
@bpfglobal
17+
def LICENSE() -> str:
18+
return "GPL"
19+
20+
21+
compile()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Adding a constant to a packet pointer is 64-bit pointer arithmetic
2+
# (r0 += 1), not the u32 addition C's integer rules would give xdp_md.data.
3+
from ctypes import c_int64
4+
from pythonbpf import bpf, section, bpfglobal, compile
5+
from vmlinux import struct_xdp_md
6+
7+
8+
@bpf
9+
@section("xdp")
10+
def prog(ctx: struct_xdp_md) -> c_int64:
11+
d = ctx.data + 1
12+
return c_int64(d)
13+
14+
15+
@bpf
16+
@bpfglobal
17+
def LICENSE() -> str:
18+
return "GPL"
19+
20+
21+
compile()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# A runtime offset added to a packet pointer is taken as an unsigned 16-bit
2+
# value (masked to 0xffff, zero-extended), so the verifier can bound the
3+
# packet range; the addition itself is 64-bit.
4+
from ctypes import c_int64
5+
from pythonbpf import bpf, section, bpfglobal, compile
6+
from pythonbpf.helper import random
7+
from vmlinux import struct_xdp_md
8+
9+
10+
@bpf
11+
@section("xdp")
12+
def prog(ctx: struct_xdp_md) -> c_int64:
13+
off = random()
14+
d = ctx.data + off
15+
return c_int64(d)
16+
17+
18+
@bpf
19+
@bpfglobal
20+
def LICENSE() -> str:
21+
return "GPL"
22+
23+
24+
compile()

‎tests/test_config.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@
6161

6262
"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"}
6363

64-
# 32-bit arithmetic on a packet-pointer context field. IR and llc succeed; the
65-
# verifier rejects it with "R0 32-bit pointer arithmetic prohibited".
66-
"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"}
64+
65+
# A packet offset has to fit the verifier's 16-bit packet range.
66+
"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"}

0 commit comments

Comments
 (0)