Skip to content

Commit 24bfb80

Browse files
Core: Access narrow map values at their own width
A lookup local was always an i64*, whatever the map's value type, so a c_uint32 value was read with an 8-byte load (the verifier rejects an access past value_size) and its spare `v_tmp` slot, typed i32, could not take the i64 a rebind stored into it. The local now points at the value's own width for an integer value (a struct value keeps the generic pointer; field access casts it), and the generic pointer the lookup helper returns is cast to it on assignment. The sites that read the local by its pointer type follow: - A comparison reads it as get_typed_operand does, at its width and with the map's declared sign, so `u == 3` on a c_uint32 value is a zext and an unsigned compare where needed (on c_uint64 values it was a signed compare before). - print() takes the format from the declared value type and widens by its sign: a c_uint8 value was about to print as a string (%s on i8*), and integers narrower than 32 bits now print as %d, promoted as C varargs are. - A rebind converts the i64 result to the slot's width. holds_map_int_value names the predicate those sites share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent dc628ee commit 24bfb80

7 files changed

Lines changed: 150 additions & 22 deletions

File tree

‎pythonbpf/allocation_pass.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,14 @@ def _allocate_for_map_method(
228228
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
229229
return
230230

231-
# Main variable (pointer to pointer)
232-
ir_type = ir.PointerType(ir.IntType(64))
231+
# Main variable (pointer to pointer). An integer value is pointed at with
232+
# its own width, so that reads and writes through it are value-sized: the
233+
# verifier rejects an 8-byte access to a 4-byte map value. A struct value
234+
# keeps the generic i64*; field access casts it to the struct type.
235+
if isinstance(value_ir_type, ir.IntType):
236+
ir_type = ir.PointerType(ir.IntType(value_ir_type.width))
237+
else:
238+
ir_type = ir.PointerType(ir.IntType(64))
233239
var = builder.alloca(ir_type, name=var_name)
234240
local_sym_tab[var_name] = LocalSymbol(var, ir_type, value_type)
235241
# Temporary variable for computed values

‎pythonbpf/assign_pass.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,24 @@ def handle_variable_assignment(
226226
logger.info(
227227
f"Creating temporary variable for pointer assignment to {var_name}"
228228
)
229-
var_ptr_tmp = local_sym_tab[f"{var_name}_tmp"].var
230-
builder.store(val, var_ptr_tmp)
231-
val = var_ptr_tmp
229+
tmp = local_sym_tab[f"{var_name}_tmp"]
230+
# The spare slot has the map value's width; expressions are i64.
231+
builder.store(convert(builder, val, val_type, tmp.ir_type), tmp.var)
232+
val = tmp.var
232233
else:
233234
logger.error(
234235
f"Type mismatch for variable {var_name}: {val_type} vs {var_type}"
235236
)
236237
return False
237238

239+
if (
240+
isinstance(val.type, ir.PointerType)
241+
and isinstance(var_type, ir.PointerType)
242+
and val.type != var_type
243+
):
244+
# A map lookup returns a generic pointer; the local is typed by the
245+
# map's value, so the pointer is cast to it (no instruction in BPF).
246+
val = builder.bitcast(val, var_type)
238247
builder.store(val, var_ptr)
239248
logger.info(f"Assigned value to variable {var_name}")
240249
return True

‎pythonbpf/expr/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .expr_pass import eval_expr, handle_expr, get_typed_operand
1+
from .expr_pass import eval_expr, handle_expr, get_typed_operand, holds_map_int_value
22
from .type_normalization import (
33
convert_to_bool,
44
get_base_type_and_depth,
@@ -20,6 +20,7 @@
2020
"canonicalise",
2121
"to_promoted",
2222
"get_typed_operand",
23+
"holds_map_int_value",
2324
"usual_arithmetic_conversions",
2425
"get_base_type_and_depth",
2526
"deref_to_depth",

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -382,25 +382,41 @@ def _handle_ctypes_call(
382382
return value, expected_type
383383

384384

385+
def holds_map_int_value(sym):
386+
"""Whether a local is a lookup result on a map with an integer value: a
387+
pointer to the value, with the value's ctype as its metadata. Such a local
388+
is used by value, read through the pointer (see get_typed_operand)."""
389+
return (
390+
isinstance(sym.ir_type, ir.PointerType)
391+
and isinstance(sym.metadata, str)
392+
and is_ctypes(sym.metadata)
393+
)
394+
395+
396+
def _compare_operand(func, compilation_context, builder, operand, local_sym_tab):
397+
"""Evaluate one side of a comparison. A map-lookup local compares by its
398+
value, read at the map value's width and typed by its declared ctype,
399+
exactly as it is read as an arithmetic operand."""
400+
if (
401+
isinstance(operand, ast.Name)
402+
and operand.id in local_sym_tab
403+
and holds_map_int_value(local_sym_tab[operand.id])
404+
):
405+
return get_typed_operand(
406+
func, compilation_context, operand, builder, local_sym_tab
407+
)
408+
return eval_expr(func, compilation_context, builder, operand, local_sym_tab)
409+
410+
385411
def _handle_compare(func, compilation_context, builder, cond, local_sym_tab):
386412
"""Handle ast.Compare expressions."""
387413

388414
if len(cond.ops) != 1 or len(cond.comparators) != 1:
389415
logger.error("Only single comparisons are supported")
390416
return None
391-
lhs = eval_expr(
392-
func,
393-
compilation_context,
394-
builder,
395-
cond.left,
396-
local_sym_tab,
397-
)
398-
rhs = eval_expr(
399-
func,
400-
compilation_context,
401-
builder,
402-
cond.comparators[0],
403-
local_sym_tab,
417+
lhs = _compare_operand(func, compilation_context, builder, cond.left, local_sym_tab)
418+
rhs = _compare_operand(
419+
func, compilation_context, builder, cond.comparators[0], local_sym_tab
404420
)
405421

406422
if lhs is None or rhs is None:

‎pythonbpf/helper/printk_formatter.py‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
import logging
33

44
from llvmlite import ir
5-
from pythonbpf.expr import eval_expr, get_base_type_and_depth, deref_to_depth, convert
5+
from pythonbpf.expr import (
6+
eval_expr,
7+
get_base_type_and_depth,
8+
deref_to_depth,
9+
convert,
10+
get_typed_operand,
11+
holds_map_int_value,
12+
)
13+
from pythonbpf.type_deducer import ctypes_to_ir
614
from pythonbpf.expr.vmlinux_registry import VmlinuxHandlerRegistry
715
from pythonbpf.helper.helper_utils import get_char_array_ptr_and_size
816

@@ -107,7 +115,13 @@ def _process_name_in_fval(
107115
):
108116
"""Process name nodes in formatted values."""
109117
if local_sym_tab and name_node.id in local_sym_tab:
110-
_, var_type, tmp = local_sym_tab[name_node.id]
118+
sym = local_sym_tab[name_node.id]
119+
# A map-lookup local prints as its value, of the map's value type;
120+
# its pointer type alone would make a c_uint8 value look like a string.
121+
if holds_map_int_value(sym):
122+
var_type = ctypes_to_ir(sym.metadata)
123+
else:
124+
var_type = sym.ir_type
111125
_populate_fval(var_type, name_node, fmt_parts, exprs)
112126
elif name_node.id in compilation_context.bpf_globals:
113127
var_type = compilation_context.bpf_globals[name_node.id].ir_type
@@ -159,7 +173,8 @@ def _populate_fval(ftype, node, fmt_parts, exprs):
159173
if ftype.width == 64:
160174
fmt_parts.append("%lld")
161175
exprs.append(node)
162-
elif ftype.width == 32:
176+
elif ftype.width <= 32:
177+
# Narrower integers are promoted to int, as C varargs do.
163178
fmt_parts.append("%d")
164179
exprs.append(node)
165180
else:
@@ -229,6 +244,18 @@ def _prepare_expr_args(expr, func, compilation_context, builder, local_sym_tab):
229244
if char_array_ptr:
230245
return char_array_ptr
231246

247+
# A map-lookup local: its value, widened per the map's value type
248+
if (
249+
isinstance(expr, ast.Name)
250+
and local_sym_tab
251+
and expr.id in local_sym_tab
252+
and holds_map_int_value(local_sym_tab[expr.id])
253+
):
254+
val, ty = get_typed_operand(
255+
func, compilation_context, expr, builder, local_sym_tab
256+
)
257+
return _handle_int_arg(val, builder, ty)
258+
232259
# Regular expression evaluation
233260
val, _ = eval_expr(func, compilation_context, builder, expr, local_sym_tab)
234261

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Map values narrower than 64 bits are read and written at their own width:
2+
# the verifier rejects an 8-byte access to a 4-byte value. Each value is then
3+
# widened per its declared sign -- zext for c_uint32 and c_uint8, sext for
4+
# c_int32 -- in arithmetic, comparisons and printing alike, and a c_uint8
5+
# value prints as a number, not as a string.
6+
from pythonbpf import bpf, map, section, bpfglobal, compile
7+
from pythonbpf.maps import HashMap
8+
from ctypes import c_void_p, c_int64, c_uint32, c_int32, c_uint8
9+
10+
11+
@bpf
12+
@map
13+
def u32s() -> HashMap:
14+
return HashMap(key=c_uint32, value=c_uint32, max_entries=4)
15+
16+
17+
@bpf
18+
@map
19+
def i32s() -> HashMap:
20+
return HashMap(key=c_uint32, value=c_int32, max_entries=4)
21+
22+
23+
@bpf
24+
@map
25+
def u8s() -> HashMap:
26+
return HashMap(key=c_uint32, value=c_uint8, max_entries=4)
27+
28+
29+
@bpf
30+
@section("tracepoint/raw_syscalls/sys_enter")
31+
def prog(ctx: c_void_p) -> c_int64:
32+
k = c_uint32(0)
33+
u = u32s.lookup(k)
34+
i = i32s.lookup(k)
35+
b = u8s.lookup(k)
36+
if u:
37+
if u == 3:
38+
print(f"u {u}")
39+
u32s.update(k, u + 1)
40+
if i:
41+
n = i - 1
42+
print(f"i {i} n {n}")
43+
if b:
44+
print(f"b {b}")
45+
b = b + 2
46+
return b
47+
return c_int64(0)
48+
49+
50+
@bpf
51+
@bpfglobal
52+
def LICENSE() -> str:
53+
return "GPL"
54+
55+
56+
compile()

‎tests/test_signedness_ir.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@
8181
[r"trunc i64 .* to i32", r"zext i32 .* to i64"],
8282
[r"sext i32 .* to i64"],
8383
),
84+
# A narrow map value is loaded at its width, widened per its sign, and
85+
# never read with an 8-byte load; a c_uint8 value is printed by value.
86+
"signedness/map_value_narrow.py": (
87+
[
88+
r"load i32, i32\* %\"deref",
89+
r"load i8, i8\* %\"deref",
90+
r"zext i32 %\"deref",
91+
r"sext i32 %\"deref",
92+
r"zext i8 %\"deref",
93+
r"store i8 .*, i8\* %\"b_tmp\"",
94+
],
95+
[r"load i64, i64\* %\"deref", r"i8\* %\"deref[^\"]*\"\)$"],
96+
),
8497
# `return p` on a map lookup dereferences through a null check and returns
8598
# the i64, never the pointer.
8699
"return/map_value.py": (

0 commit comments

Comments
 (0)