Skip to content

Commit f4ec12d

Browse files
r41k0uclaude
andcommitted
Core: Resolve assignment targets local-first, and reject global on a parameter
Reads resolve a name local-first (_handle_name_expr, get_operand_value, the printk formatter), but both write paths checked the declared-global set before local_sym_tab. Normally harmless because the two tables are disjoint by construction -- except for a parameter whose name matches a @bpfglobal. With 'global ctx' in a function taking ctx, the write path stored to the global while a read of the same name picked the parameter and crashed on its None slot: the same name resolving to different storage depending on which side of an assignment it sat. Python forbids the construct outright, and now so does the compiler, with Python's own wording: SyntaxError: name 'ctx' is parameter and global. Both write paths now resolve local-first like the reads, so all name lookups read the same way; and an augmented assignment to the context parameter itself gets a clear error instead of a store to None. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent f5c4dec commit f4ec12d

4 files changed

Lines changed: 53 additions & 5 deletions

File tree

pythonbpf/assign_pass.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ def handle_variable_assignment(
115115

116116
# A name declared with `global` writes the @bpfglobal symbol directly:
117117
# the plain `store i64 %v, ptr @counter` form of the C reference.
118-
if var_name in compilation_context.current_func_globals:
118+
if (
119+
var_name not in local_sym_tab
120+
and var_name in compilation_context.current_func_globals
121+
):
119122
sym = compilation_context.bpf_globals[var_name]
120123
val_result = eval_expr(func, compilation_context, builder, rval, local_sym_tab)
121124
if val_result is None:

pythonbpf/functions/functions_pass.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,25 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
205205
"""
206206
if isinstance(stmt.target, ast.Name):
207207
name = stmt.target.id
208-
if name in compilation_context.current_func_globals:
208+
# Same resolution order as reads: local, then declared global. The two
209+
# cannot both hold a name (a parameter may not be declared global, and
210+
# an undeclared write to a global's name is refused), so this order is
211+
# about giving the same answer as _handle_name_expr, not precedence.
212+
if name in local_sym_tab:
213+
slot = local_sym_tab[name].var
214+
slot_type = local_sym_tab[name].ir_type
215+
if slot is None:
216+
raise SyntaxError(
217+
f"cannot assign to '{name}': it is the context parameter"
218+
)
219+
elif name in compilation_context.current_func_globals:
209220
sym = compilation_context.bpf_globals[name]
210221
slot, slot_type = sym.var, sym.ir_type
211222
elif name in compilation_context.bpf_globals:
212223
raise SyntaxError(
213224
f"augmented assignment to '{name}' shadows the BPF global of "
214225
f"the same name — add 'global {name}' to write to it"
215226
)
216-
elif name in local_sym_tab:
217-
slot = local_sym_tab[name].var
218-
slot_type = local_sym_tab[name].ir_type
219227
else:
220228
raise SyntaxError(f"augmented assignment to undefined variable '{name}'")
221229
elif isinstance(stmt.target, ast.Attribute) and isinstance(
@@ -411,9 +419,15 @@ def process_func_body(
411419
# @bpfglobal, never a local. Undeclared writes to a global name are
412420
# rejected in the allocation pass rather than silently shadowing.
413421
declared_globals: set[str] = set()
422+
param_names = {arg.arg for arg in func_node.args.args}
414423
for node in ast.walk(func_node):
415424
if isinstance(node, ast.Global):
416425
for gname in node.names:
426+
if gname in param_names:
427+
# Python's own rule and wording. Without it the read path
428+
# (local first) and the write path would resolve the same
429+
# name to different storage.
430+
raise SyntaxError(f"name '{gname}' is parameter and global")
417431
if gname not in compilation_context.bpf_globals:
418432
raise SyntaxError(
419433
f"'global {gname}' in '{func_node.name}': no @bpfglobal "
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Declaring a parameter `global` is a SyntaxError in Python itself
2+
# ("name 'ctx' is parameter and global"), and the compiler must say the same:
3+
# otherwise reads (local first) and writes would resolve `ctx` to different
4+
# storage.
5+
from pythonbpf import bpf, section, bpfglobal, compile
6+
from ctypes import c_void_p, c_int64, c_uint64
7+
8+
9+
@bpf
10+
@bpfglobal
11+
def ctx() -> c_uint64:
12+
return c_uint64(0)
13+
14+
15+
@bpf
16+
@section("tracepoint/raw_syscalls/sys_enter")
17+
def prog(ctx: c_void_p) -> c_int64: # noqa: F811 -- the collision is the test
18+
global ctx # noqa: F811
19+
ctx += 1
20+
return c_int64(0)
21+
22+
23+
@bpf
24+
@bpfglobal
25+
def LICENSE() -> str:
26+
return "GPL"
27+
28+
29+
compile()

tests/test_config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@
4040
"failing_tests/globals_bad_type.py" = {reason = "Non-integer-scalar globals are not supported in milestone 1 (NotImplementedError by design)", level = "ir"}
4141

4242
"failing_tests/globals_augassign_shadowing.py" = {reason = "Augmented assignment to a global name without a `global` statement is a deliberate compile error (would shadow the BPF global)", level = "ir"}
43+
44+
"failing_tests/globals_parameter_and_global.py" = {reason = "A parameter may not be declared global (Python: name is parameter and global)", level = "ir"}

0 commit comments

Comments
 (0)