Skip to content

Commit 8d46f83

Browse files
Core: Fix loop edge cases the lowering got wrong
- `pass` is accepted as a statement, the natural body of a loop run for its side effects. - Falling off the end of a function returns 0 in the function's own return type. After `while True` that block is unreachable, but a c_int32 function still emitted `ret i64 0`, which llc rejected. - A range() step other than 1 could wrap the counter past INT64_MAX or UINT64_MAX back inside the range and loop forever. The step block now ends the loop when the distance left to stop, exact as an unsigned number, is no longer than the step. That is a normal exit, so the else-branch runs. Steps of 1 cannot overshoot and keep the plain loop, so the helper-bounded loop the verifier walks is unchanged. - A step of 2**64 or more no longer truncates to 0: it is a compile error, since it does not fit the counter. Steps from 2**63 up are emitted by their bits. - The counter's signedness folds each bound into a signed 64-bit type rather than joining the bounds alone, so only a c_uint64 bound makes it unsigned: range(-1, n) over a c_uint32 n runs 4 times, as in C with an __s64 counter.
1 parent 89b7eb6 commit 8d46f83

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

‎pythonbpf/allocation_pass.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,19 @@ def handle_for_allocation(compilation_context, builder, stmt, local_sym_tab):
102102
start, stop, step = parse_range(stmt)
103103

104104
# range() yields Python ints; like any undeclared local they are 64-bit,
105-
# signed unless the bounds make C's arithmetic unsigned.
105+
# signed unless the bounds make C's arithmetic unsigned. That arithmetic
106+
# is the counter's against each bound, so it starts from a signed 64-bit
107+
# counter, and only an unsigned 64-bit bound turns it unsigned: every
108+
# narrower unsigned bound fits in the signed counter, as in C.
106109
bound_types = [
107110
infer_int_type(bound, local_sym_tab, compilation_context)
108111
for bound in (start, stop)
109112
if bound is not None
110113
]
111114
signed = True
112115
if all(ty is not None for ty in bound_types):
113-
common = bound_types[0]
114-
for ty in bound_types[1:]:
116+
common = IntTy(64, True)
117+
for ty in bound_types:
115118
common = usual_arithmetic_conversions(common, ty)
116119
signed = signedness(common)
117120
if not signed and step < 0:
@@ -191,6 +194,11 @@ def parse_range(stmt):
191194
step = -literal.value if negate else literal.value
192195
if step == 0:
193196
raise ValueError(f"range() arg 3 must not be zero (line {stmt.lineno})")
197+
if abs(step) >= 1 << 64:
198+
raise ValueError(
199+
f"range() step on line {stmt.lineno} does not fit the 64-bit "
200+
f"loop counter: {ast.unparse(step_node)}"
201+
)
194202
return start, stop, step
195203

196204

‎pythonbpf/functions/functions_pass.py‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,22 @@ def bound(expr):
491491
)
492492

493493
builder.position_at_end(inc_block)
494-
next_idx = builder.add(builder.load(counter.var), ir.Constant(loop_ty, step))
494+
idx = builder.load(counter.var)
495+
if abs(step) > 1:
496+
# The body only runs with the counter strictly inside the range, so
497+
# the distance left to stop is exact as an unsigned number. A step no
498+
# shorter than that distance ends the loop here, before the add can
499+
# wrap the counter back inside the range (a step of 1 cannot
500+
# overshoot stop, so it keeps the plain loop).
501+
left = builder.sub(stop_val, idx) if step > 0 else builder.sub(idx, stop_val)
502+
step_in_range = func.append_basic_block(name="for.step")
503+
builder.cbranch(
504+
builder.icmp_unsigned(">", left, ir.Constant(loop_ty, _wrap64(abs(step)))),
505+
step_in_range,
506+
else_block or end_block,
507+
)
508+
builder.position_at_end(step_in_range)
509+
next_idx = builder.add(idx, ir.Constant(loop_ty, _wrap64(step)))
495510
builder.store(next_idx, counter.var)
496511
builder.branch(cond_block)
497512

@@ -509,6 +524,13 @@ def bound(expr):
509524
)
510525

511526

527+
def _wrap64(value):
528+
"""`value` modulo 2**64, as the signed Python int LLVM parses for an i64:
529+
a step of 2**63 or more (or its negation) is written by its bits."""
530+
value %= 1 << 64
531+
return value - (1 << 64) if value >= 1 << 63 else value
532+
533+
512534
def handle_loop_jump(compilation_context, builder, stmt):
513535
"""Handle `break` and `continue`: branch to the innermost loop's exit or
514536
next-iteration block."""
@@ -595,6 +617,8 @@ def process_stmt(
595617
handle_for(func, compilation_context, builder, stmt, local_sym_tab, ret_type)
596618
elif isinstance(stmt, (ast.Break, ast.Continue)):
597619
handle_loop_jump(compilation_context, builder, stmt)
620+
elif isinstance(stmt, ast.Pass):
621+
pass
598622
elif isinstance(stmt, ast.Return):
599623
did_return = handle_return(
600624
func, builder, stmt, local_sym_tab, ret_type, compilation_context
@@ -734,7 +758,11 @@ def process_func_body(
734758
)
735759

736760
if not did_return:
737-
builder.ret(ir.Constant(ir.IntType(64), 0))
761+
# Falling off the end returns 0, in the function's own return type:
762+
# after `while True` this block is unreachable, but it still has to
763+
# type-check.
764+
default_ty = ret_type if isinstance(ret_type, ir.IntType) else ir.IntType(64)
765+
builder.ret(ir.Constant(default_ty, 0))
738766

739767

740768
def process_bpf_chunk(func_node, compilation_context, return_type):

0 commit comments

Comments
 (0)