-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[BPF] Handle nested wrapper structs in BPF map definition traversal #144097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
llvm/lib/Target/BPF/BTFDebug.cpp
Outdated
for (const auto *Element : Elements) { | ||
const auto *MemberType = cast<DIDerivedType>(Element); | ||
visitTypeEntry(MemberType->getBaseType()); | ||
if (IsAWrapperType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doing this check inside the loop is a bit odd, since it will only ever happen if the loop runs for exactly one iteration. at the very least I think this should move outside the loop body, but also I wonder if there's a more sensible way to do this. What if we decide to have more than one element in the wrapper struct in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but also I wonder if there's a more sensible way to do this.
Good point. Assuming that BPF map definitions should have only pointers and arrays, maybe we could assume that any DICompositeType
element is a wrapper?
What if we decide to have more than one element in the wrapper struct in the future?
Hard to imagine that happening, but the idea of recursively looking into DICompositeType
elements should solve it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, with the new logic (detecting whether a given field holds DICompositeType
), the if statement has to be inside the loop. PTAL
392a256
to
1a0a383
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the BTF map-definition traversal in visitMapDefType
to correctly descend through nested wrapper structs and ensure inner pointee types are fully visited.
- Detect composite wrapper members and recurse with
visitMapDefType
instead ofvisitTypeEntry
. - Preserve generic type visitation for non-composite members.
- Updated comments to explain the new traversal logic.
Comments suppressed due to low confidence (1)
llvm/lib/Target/BPF/BTFDebug.cpp:991
- [nitpick] The variable name
MemberCTy
is abbreviated; consider renaming it toMemberCompositeType
for improved clarity.
const auto *MemberCTy = dyn_cast<DICompositeType>(MemberBaseType);
@@ -0,0 +1,606 @@ | |||
; RUN: llc -mtriple=bpfel -filetype=asm -o - %s | FileCheck -check-prefixes=CHECK-SHORT %s | |||
; RUN: llc -mtriple=bpfel -filetype=asm -o - %s | FileCheck -check-prefixes=CHECK %s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please take a look at the test case llvm/test/CodeGen/BPF/BTF/unreachable.ll
?
There and in a few other places we started using a small python script, print_btf.py
, to print out BTF in tests in a readable form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do that, I completely missed the print_btf.py
utility.
I wrote this test in the same style as the other map-def*.ll
tests, which unfortunately don't use print_btf.py
and perform manual assembly assertions. Would you be open for switching them as well? I could create a new PR for that.
; RUN: llc -mtriple=bpfel -filetype=asm -o - %s | FileCheck -check-prefixes=CHECK-SHORT %s | ||
; RUN: llc -mtriple=bpfel -filetype=asm -o - %s | FileCheck -check-prefixes=CHECK %s | ||
|
||
; Source code: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please try to minimize your test case?
As far as I understand, the intended use case looks as follows in C:
$ cat test_struct_dbg.c
struct key { int i; };
struct val { int j; };
#define __uint(name, val) int (*name)[val]
#define __type(name, val) typeof(val) *name
struct {
__uint(type, 1);
__uint(max_entries, 1);
__type(key, struct key);
__type(value, struct val);
} map __attribute__((section(".maps")));
And it generates a much smaller amount of IR:
$ clang -g -emit-llvm -S test_struct_dbg.c -o - | wc -l
45
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such test wouldn't be really relevant to the issue that this PR is trying to fix. The simple, not nested map structs in C, like the one you wrote, always worked fine.
What doesn't work is wrapping such map structs in nested structs, which is necessary in Rust. I tried my best explaining that in the PR description and commit message, please let me know if something is unclear there.
The C code, capable of reproducing the issue, would look like:
struct hash_map {
struct {
__uint(type, 1);
__uint(max_entries, 1);
__type(key, struct key);
__type(value, struct val);
} __0;
}:
struct hash_map map __attribute__((section(".maps")));
where the actual map definition is wrapped in some other type.
I don't understand where the |
A side note: is there a way to kick Copilot out of reviewers? It just produces random noise. |
IIUC, rust compiler will generate a struct chain like 'HashMap.__0.UnsafeCall.value.HashMapDef' and the last one 'HashMapDef' is the actual hash map definition based on C code. From that perspective, yes, your patch MAY make sense since all previous structs like 'HashMap', '__0', 'UnsafeCall' and 'value' are all internally created by rust for some safety checking so recursively going through visitMapDefType() does make sense for this case. Please provide a simpler test with only necessary part so it can be checked easily. Also please use BTF/print_btf.py as @eddyz87 suggested. |
It comes from the pub struct HashMap<K, V, const M: usize, const F: usize = 0>(
core::cell::UnsafeCell<HashMapDef<K, V, M, F>>,
); Such struct, without named fields, is a "tuple struct". Rust assigns the field names We define the
pub struct UnsafeCell<T: ?Sized> {
value: T,
} We hold the actual BPF map definition in that Given all of that, as @yonghong-song said, the chain is |
That's exactly right.
Yes, that was my way of thinking.
Unfortunately, the Rust code I used for generating the IR is the simpliest one which reproduces the issue and actually compiles. To be precise, this is the code: #![no_std]
#![no_main]
#![allow(dead_code)]
pub const BPF_MAP_TYPE_HASH: usize = 1;
// The real map definition.
pub struct HashMapDef<K, V, const M: usize, const F: usize> {
r#type: *const [i32; BPF_MAP_TYPE_HASH],
key: *const K,
value: *const V,
max_entries: *const [i32; M],
map_flags: *const [i32; F],
}
impl<K, V, const M: usize, const F: usize> HashMapDef<K, V, M, F> {
pub const fn new() -> Self {
Self {
r#type: &[0i32; BPF_MAP_TYPE_HASH],
key: ::core::ptr::null(),
value: ::core::ptr::null(),
max_entries: &[0i32; M],
map_flags: &[0i32; F],
}
}
}
// Use `UnsafeCell` to allow the mutability by multiple threads.
pub struct HashMap<K, V, const M: usize, const F: usize = 0>(
core::cell::UnsafeCell<HashMapDef<K, V, M, F>>,
);
impl<K, V, const M: usize, const F: usize> HashMap<K, V, M, F> {
pub const fn new() -> Self {
Self(core::cell::UnsafeCell::new(HashMapDef::new()))
}
}
/// Tell Rust that `HashMap` is thread-safe.
unsafe impl<K: Sync, V: Sync, const M: usize, const F: usize> Sync for HashMap<K, V, M, F> {}
// Define custom structs for key and values.
pub struct MyKey(u32);
pub struct MyValue(u32);
#[link_section = ".maps"]
#[export_name = "HASH_MAP"]
pub static HASH_MAP: HashMap<MyKey, MyValue, 1337> = HashMap::new();
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
} Everything except the All the types present in LLVM DebugInfo which don't look obviously related to the map types, are either dependencies of
Sure! Thanks for letting me know about the tool. I was mostly looking at other |
Hmm... when using
I haven't seen anything like that when using btfdump on the same object file, generated from the same Rust code. Do we have a strict requirement of PTR types being anonymous? I will try to figure that out on my own, but if you have any ideas what's wrong here, please let me know. |
Nevermind, I figured that out. Named pointer types are totally normal in Rust and the Linux kernel might have some requirement around that, given that we already have a fixup for that in bpf-linker: But I think we should stop enforcing that. If the kernel really has such a requirement, I'm happy to prepare a fix and propose it to ML. I will take a look this week. What I can say for now, is that on LLVM's side, emitting BTF with named pointers works just fine. |
I took a quick look at |
I mean, you can use my C example, it tests the same thing:
|
Disregard my previous comment, here is a point in the kernel source code that checks for pointers to be w/o names:
Function |
OK, I can write an example in C which is capable of reproducing the issue. To be precise, the map struct itself needs to be nested, so I will try something like: struct key { int i; };
struct val { int j; };
#define __uint(name, val) int (*name)[val]
#define __type(name, val) typeof(val) *name
struct {
struct {
__uint(type, 1);
__uint(max_entries, 1);
__type(key, struct key);
__type(value, struct val);
} map_def;
} map __attribute__((section(".maps"))); On a side note, I know that such nested definition doesn't work on libbpf. I made it work in Aya by modyfing the logic for parsing BTF maps. It works fine and the kernel is able to work with such program. I'm happy to port that to libbpf eventually, if there is interest.
I see. Would you be still open to relaxing |
Noted, I think it's fine, this is a unit test for some backend functionality it doesn't need to be something that fully works end-to-end from Rust/C to kernel.
libbpf is C oriented and I don't think there is a use-case, but you can always ask on the mailing list.
The name is printed anyways, so signal is not lost => removing the warning from
As I said, there is no functional reason for pointer types to have names in BTF, any strings encoding such names would unnecessarily use space in the .BTF strings section. Hence, I think upstream kernel might object, but who knows. By the way, I encourage you to make a small test for yourself, there might be some differences between BTF validation rules applied to kernel/module BTF and program BTF. |
OK, the point about no functional reason for named pointers sounds reasonable. Named pointer types in Rust are convenient for debuggers, but are indeed of no use in the kernel/BPF world. And the goal of BTF is to be small. That makes me think of a completely opposite idea - what if we remove pointer names here, in the BPF backend in LLVM? Since named pointers are correct from the point of view of LLVM debug info, but we don't want them to bloat BTF, then the LLVM backend seems like a correct place for stripping unnecessary information. |
We can disregard my last comment, at least in the context of this PR. After rewriting the codegen test in C, there are no named pointers, so The main issue (supporting nested maps) is anyway fixed. @eddyz87 @yonghong-song Please let me know if the changes look good to you now. |
I did a sanity check with BPF selftests, all are passing. Also there are no significant changes in the generated BTF (there is some slight reordering, but that's not important). |
This can be done, makes sense to me. |
I don't have commit access, so somebody else needs to merge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have commit access, so somebody else needs to merge
I don't anymore either.
!1 = distinct !DIGlobalVariable(name: "map", scope: !2, file: !3, line: 14, type: !5, isLocal: false, isDefinition: true) | ||
!2 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, producer: "clang version 21.0.0git ([email protected]:vadorovsky/llvm-project.git c935bd3798b39330aab2c9ca29a519457d5e5245)", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, globals: !4, splitDebugInlining: false, nameTableKind: None) | ||
!3 = !DIFile(filename: "bpf.c", directory: "/home/vadorovsky/playground/btf", checksumkind: CSK_MD5, checksum: "2330cce6d83c72ef5335abc3016de28e") | ||
!4 = !{!0} | ||
!5 = distinct !DICompositeType(tag: DW_TAG_structure_type, file: !3, line: 7, size: 256, elements: !6) | ||
!6 = !{!7} | ||
!7 = !DIDerivedType(tag: DW_TAG_member, name: "map_def", scope: !5, file: !3, line: 13, baseType: !8, size: 256) | ||
!8 = distinct !DICompositeType(tag: DW_TAG_structure_type, scope: !5, file: !3, line: 8, size: 256, elements: !9) | ||
!9 = !{!10, !16, !21, !26} | ||
!10 = !DIDerivedType(tag: DW_TAG_member, name: "type", scope: !8, file: !3, line: 9, baseType: !11, size: 64) | ||
!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !12, size: 64) | ||
!12 = !DICompositeType(tag: DW_TAG_array_type, baseType: !13, size: 32, elements: !14) | ||
!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) | ||
!14 = !{!15} | ||
!15 = !DISubrange(count: 1) | ||
!16 = !DIDerivedType(tag: DW_TAG_member, name: "max_entries", scope: !8, file: !3, line: 10, baseType: !17, size: 64, offset: 64) | ||
!17 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !18, size: 64) | ||
!18 = !DICompositeType(tag: DW_TAG_array_type, baseType: !13, size: 42784, elements: !19) | ||
!19 = !{!20} | ||
!20 = !DISubrange(count: 1337) | ||
!21 = !DIDerivedType(tag: DW_TAG_member, name: "key", scope: !8, file: !3, line: 11, baseType: !22, size: 64, offset: 128) | ||
!22 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !23, size: 64) | ||
!23 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "key", file: !3, line: 1, size: 32, elements: !24) | ||
!24 = !{!25} | ||
!25 = !DIDerivedType(tag: DW_TAG_member, name: "i", scope: !23, file: !3, line: 1, baseType: !13, size: 32) | ||
!26 = !DIDerivedType(tag: DW_TAG_member, name: "value", scope: !8, file: !3, line: 12, baseType: !27, size: 64, offset: 192) | ||
!27 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !28, size: 64) | ||
!28 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "val", file: !3, line: 2, size: 32, elements: !29) | ||
!29 = !{!30} | ||
!30 = !DIDerivedType(tag: DW_TAG_member, name: "j", scope: !28, file: !3, line: 2, baseType: !13, size: 32) | ||
!31 = !{i32 7, !"Dwarf Version", i32 5} | ||
!32 = !{i32 2, !"Debug Info Version", i32 3} | ||
!33 = !{i32 1, !"wchar_size", i32 4} | ||
!34 = !{i32 7, !"frame-pointer", i32 2} | ||
!35 = !{!"clang version 21.0.0git ([email protected]:vadorovsky/llvm-project.git c935bd3798b39330aab2c9ca29a519457d5e5245)"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some paths leaked into this, is that typical?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is if you use clang built from local sources - usually the git URI gets applied to the version string. I can sanitize it - maybe by changing it to llvm/llvm-project
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!7 = !{!"clang version 20.1.6 (https://github.com/llvm/llvm-project.git aa804fd3e624cb92c6e7665182504c6049387f35)"} |
looks like that's more common.
not also the occurrences on line 84 and 85.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, done. I also sanitized the source dir to /tmp.
In Aya/Rust, BPF map definitions are nested in two nested types: * A struct representing the map type (e.g., `HashMap`, `RingBuf`) that provides methods for interacting with the map type (e.g. `HashMap::get`, `RingBuf::reserve`). * An `UnsafeCell`, which informs the Rust compiler that the type is thread-safe and can be safely mutated even as a global variable. The kernel guarantees map operation safety. This leads to a type hierarchy like: ```rust pub struct HashMap<K, V, const M: usize, const F: usize = 0>( core::cell::UnsafeCell<HashMapDef<K, V, M, F>>, ); const BPF_MAP_TYPE_HASH: usize = 1; pub struct HashMapDef<K, V, const M: usize, const F: usize = 0> { r#type: *const [i32; BPF_MAP_TYPE_HASH], key: *const K, value: *const V, max_entries: *const [i32; M], map_flags: *const [i32; F], } ``` Then used in the BPF program code as a global variable: ```rust #[unsafe(link_section = ".maps")] #[unsafe(export_name = "HASH_MAP")] static HASH_MAP: HashMap<u32, u32, 1337> = HashMap::new(); ``` Which is an equivalent of the following BPF map definition in C: ```c #define BPF_MAP_TYPE_HASH 1 struct { int (*type)[BPF_MAP_TYPE_HASH]; typeof(int) *key; typeof(int) *value; int (*max_entries)[1337]; } map_1 __attribute__((section(".maps"))); ``` Accessing the actual map definition requires traversing: ``` HASH_MAP -> __0 -> value ``` Previously, the BPF backend only visited the pointee types of the outermost struct, and didn’t descend into inner wrappers. This caused issues when the key/value types were custom structs: ```rust // Define custom structs for key and values. pub struct MyKey(u32); pub struct MyValue(u32); #[unsafe(link_section = ".maps")] #[unsafe(export_name = "HASH_MAP")] pub static HASH_MAP: HashMap<MyKey, MyValue, 1337> = HashMap::new(); ``` These types weren’t fully visited and appeared in BTF as forward declarations: ``` llvm#30: <FWD> 'MyKey' kind:struct llvm#31: <FWD> 'MyValue' kind:struct ``` The fix is to enhance `visitMapDefType` to recursively visit inner composite members. If a member is a composite type (likely a wrapper), it is now also visited using `visitMapDefType`, ensuring that the pointee types of the innermost stuct members, like `MyKey` and `MyValue`, are fully resolved in BTF. With this fix, the correct BTF entries are emitted: ``` llvm#6: <STRUCT> 'MyKey' sz:4 n:1 #00 '__0' off:0 --> [7] llvm#7: <INT> 'u32' bits:32 off:0 llvm#8: <PTR> --> [9] llvm#9: <STRUCT> 'MyValue' sz:4 n:1 #00 '__0' off:0 --> [7] ``` Fixes: llvm#143361
Ok, the change looks okay to me. Echoing to the previous comment from @eddyz87, nested wrapper structs won't work for libbpf, but libbpf is mostly for C. We have libbpf-rs but that is just a wrapper for C version of libbpf. Since your nested wrapper is only for rust based libbpf, so it should be okay for now. |
In Aya/Rust, BPF map definitions are nested in two nested types:
HashMap
,RingBuf
) that provides methods for interacting with the map type (e.g.HashMap::get
,RingBuf::reserve
).UnsafeCell
, which informs the Rust compiler that the type is thread-safe and can be safely mutated even as a global variable. The kernel guarantees map operation safety.This leads to a type hierarchy like:
Then used in the BPF program code as a global variable:
Which is an equivalent of the following BPF map definition in C:
Accessing the actual map definition requires traversing:
Previously, the BPF backend only visited the pointee types of the outermost struct, and didn’t descend into inner wrappers. This caused issues when the key/value types were custom structs:
These types weren’t fully visited and appeared in BTF as forward declarations:
The fix is to enhance
visitMapDefType
to recursively visit inner composite members. If a member is a composite type (likely a wrapper), it is now also visited usingvisitMapDefType
, ensuring that the pointee types of the innermost stuct members, likeMyKey
andMyValue
, are fully resolved in BTF.With this fix, the correct BTF entries are emitted:
Fixes: #143361