Skip to content

Commit f2ec707

Browse files
r41k0uclaude
andcommitted
Core: Flatten anonymous struct/union members during vmlinux parsing
On this kernel `struct pt_regs` wraps `cs` and `ss` in anonymous unions. ctypes lifts those names onto the parent class, exactly as C does, but the parser only walked the top-level `_fields_`, so `cs` and `ss` were unknown to the compiler and `ctx.cs` blew up. Register the members of an anonymous member as fields of the parent, one level deep, taking the ABSOLUTE offset ctypes reports on the parent (no arithmetic) and recording the access path (anonymous member index, member index). The anonymous member itself stays registered as before. Deliberately conservative: only scalar ctypes members are flattened. Nested vmlinux structs (`fred_cs`, `fred_ss`) and bitfields are skipped with a warning rather than half-registered, and a flattened member never overwrites an existing top-level field of the same name. Overlapping members are kept as distinct fields, since `cs` and `csx` both legitimately live at offset 136. Flattened fields are not yet emitted: IR generation and DWARF generation skip them, and critically they do not consume a top-level field index, so existing CO-RE names are untouched. Every vmlinux/xdp test program still compiles to a byte-identical .ll. Entirely inert for any struct without `_anonymous_`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aa51d12 commit f2ec707

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

pythonbpf/vmlinux_parser/class_handler.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,119 @@ def unwrap_pointer_type(type_obj: Any) -> Any:
4343
return current_type
4444

4545

46+
def _is_flattenable_scalar(member_type: Any) -> bool:
47+
"""
48+
Report whether an anonymous member's member can be lifted into the parent.
49+
50+
Only plain ctypes scalars qualify. Pointers, arrays, function pointers and
51+
nested vmlinux structs are deliberately excluded: they need the containing
52+
type / array length bookkeeping that the top-level field walk does, and
53+
half-registering them would produce silently wrong relocations.
54+
"""
55+
if getattr(member_type, "__module__", None) != ctypes.__name__:
56+
return False
57+
if not isinstance(member_type, type):
58+
return False
59+
if issubclass(member_type, (ctypes._Pointer, ctypes.Array)):
60+
return False
61+
if hasattr(member_type, "_restype_") and hasattr(member_type, "_argtypes_"):
62+
return False
63+
return True
64+
65+
66+
def flatten_anonymous_members(class_obj, dep_node) -> None:
67+
"""
68+
Register the members of a struct's anonymous members as fields of the struct.
69+
70+
C lets an anonymous struct/union member's members be named directly on the
71+
parent (`regs->cs` where `cs` lives inside an anonymous union), and ctypes
72+
mirrors that by exposing them on the parent class. The top-level `_fields_`
73+
walk only sees the anonymous member itself (`_0`), so those names would
74+
otherwise be unknown to the compiler.
75+
76+
Each flattened member keeps the anonymous member registered as before, is
77+
given the ABSOLUTE offset ctypes reports for it on the parent, and records
78+
its CO-RE access path as (index of the anonymous member in the parent,
79+
index of the member within the anonymous member).
80+
81+
Members that overlap are expected and correct: in `struct pt_regs`, `cs`
82+
and `csx` are both at offset 136. Each keeps its own field.
83+
84+
This is a no-op for any struct without `_anonymous_`.
85+
"""
86+
anonymous_names = getattr(class_obj, "_anonymous_", None)
87+
if not anonymous_names:
88+
return
89+
declared_fields = getattr(class_obj, "_fields_", None)
90+
if not declared_fields:
91+
return
92+
93+
anonymous_names = set(anonymous_names)
94+
for parent_index, declared_field in enumerate(declared_fields):
95+
parent_name = declared_field[0]
96+
if parent_name not in anonymous_names:
97+
continue
98+
parent_type = declared_field[1]
99+
member_fields = getattr(parent_type, "_fields_", None)
100+
if not member_fields:
101+
logger.warning(
102+
f"Anonymous member {dep_node.name}.{parent_name} has no _fields_, "
103+
"its members will not be accessible"
104+
)
105+
continue
106+
107+
for member_index, member in enumerate(member_fields):
108+
member_name = member[0]
109+
member_type = member[1]
110+
member_bitfield_size = member[2] if len(member) == 3 else None
111+
112+
if member_name in dep_node.fields:
113+
# Never let a flattened member shadow a field we already have.
114+
logger.warning(
115+
f"Anonymous member {dep_node.name}.{parent_name}.{member_name} "
116+
f"collides with an existing field of {dep_node.name}, keeping "
117+
"the existing field"
118+
)
119+
continue
120+
if member_bitfield_size is not None:
121+
logger.warning(
122+
f"Skipping bitfield {dep_node.name}.{parent_name}.{member_name}: "
123+
"bitfields inside anonymous members are not supported yet"
124+
)
125+
continue
126+
if not _is_flattenable_scalar(member_type):
127+
logger.warning(
128+
f"Skipping {dep_node.name}.{parent_name}.{member_name} of type "
129+
f"{member_type}: only scalar ctypes members of anonymous members "
130+
"are supported"
131+
)
132+
continue
133+
if not hasattr(class_obj, member_name):
134+
# ctypes did not lift the name onto the parent, so we have no
135+
# authoritative absolute offset for it.
136+
logger.warning(
137+
f"Skipping {dep_node.name}.{parent_name}.{member_name}: ctypes "
138+
"does not expose it on the parent struct"
139+
)
140+
continue
141+
142+
dep_node.add_field(
143+
member_name,
144+
member_type,
145+
ready=False,
146+
access_path=(parent_index, member_index),
147+
)
148+
dep_node.set_field_bitfield_size(member_name, None)
149+
# set_field_ready reads the offset straight off the ctypes struct,
150+
# which reports the ABSOLUTE offset for a lifted anonymous member.
151+
dep_node.set_field_ready(member_name, is_ready=True)
152+
logger.debug(
153+
f"Flattened {dep_node.name}.{parent_name}.{member_name} at offset "
154+
f"{dep_node.fields[member_name].offset} with access path "
155+
f"{(parent_index, member_index)}"
156+
)
157+
158+
46159
def process_vmlinux_class(
47160
node,
48161
llvm_module,
@@ -312,6 +425,11 @@ def process_vmlinux_post_ast(
312425
raise ValueError(
313426
f"{elem_name} with type {elem_type} from module {module_name} not supported in recursive resolver"
314427
)
428+
429+
# Anonymous struct/union members expose their members on the parent
430+
# in C and in ctypes, but they are invisible to the top-level
431+
# _fields_ walk above. Register them too. No-op without _anonymous_.
432+
flatten_anonymous_members(class_obj, new_dep_node)
315433
elif module_name == ctypes.__name__ or module_name is None:
316434
# Handle ctypes types - these don't need processing, just return
317435
logger.debug(f"Skipping ctypes type {current_symbol_name}")

pythonbpf/vmlinux_parser/ir_gen/debug_info_gen.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ def debug_info_generation(
4343
# Process all fields and create members for the struct
4444
members = []
4545

46-
sorted_fields = sorted(struct.fields.items(), key=lambda item: item[1].offset)
46+
# Members lifted out of an anonymous member are not members of this struct in
47+
# DWARF terms; they belong to the anonymous member's own type. Emitting them
48+
# here would both duplicate offsets and shift every later member's index.
49+
sorted_fields = sorted(
50+
(item for item in struct.fields.items() if item[1].access_path is None),
51+
key=lambda item: item[1].offset,
52+
)
4753

4854
for field_name, field in sorted_fields:
4955
try:

pythonbpf/vmlinux_parser/ir_gen/ir_generation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def struct_processor(self, struct, processing_stack=None):
8585
# Create a members dictionary for AssignmentInfo
8686
members_dict = {}
8787
for field_name, field in struct.fields.items():
88+
if field.access_path is not None:
89+
# Member lifted out of an anonymous member. gen_ir does
90+
# not emit a global for it yet, so it stays unexported.
91+
continue
8892
# Get the generated field name from our dictionary, or use field_name if not found
8993
if (
9094
struct.name in self.generated_field_names
@@ -132,6 +136,10 @@ def gen_ir(self, struct, generated_debug_info):
132136
self.generated_field_names[struct.name] = {}
133137

134138
for field_name, field in struct.fields.items():
139+
if field.access_path is not None:
140+
# Member lifted out of an anonymous member. It is not a
141+
# top-level field, so it must not consume a field index.
142+
continue
135143
# does not take arrays and similar types into consideration yet.
136144
if callable(field.ctype_complex_type):
137145
# Function pointer case - generate a simple field accessor

0 commit comments

Comments
 (0)