Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d4526fd
struct_ops: add structOpsMeta to carry BTF hints for StructOpsMap cre…
shun159 Aug 10, 2025
142a1aa
struct_ops: drop unused members for now
shun159 Aug 10, 2025
94d1686
struct_ops: Add struct_ops loader
shun159 Aug 11, 2025
daaf51d
struct_ops: implement preLoad()
shun159 Aug 12, 2025
da8c896
map: fix log message
shun159 Aug 12, 2025
93366b1
struct_ops: implement onProgramLoaded() and postLoad()
shun159 Aug 13, 2025
d0f70d2
testing: remove struct_ops dummy helpers
shun159 Aug 14, 2025
9bec0f0
struct_ops: fix to remove unused struct members
shun159 Aug 14, 2025
5334dab
struct_ops: move some struct_ops logics to collection
shun159 Aug 14, 2025
b6bcad6
struct_ops: moved postLoad() logic into populateDeferredMaps()
shun159 Aug 14, 2025
6f726b3
struct_ops: move initKernStructOps() to NewCollectionWithOptions()
shun159 Aug 14, 2025
ba767e9
testing: fix to skip sturct_ops test when bpf_testmod_ops is not loaded
shun159 Aug 15, 2025
23b95ca
struct_ops: add code comments
shun159 Sep 6, 2025
9736468
struct_ops: switch to AttachTo for program binding
shun159 Sep 13, 2025
4aa4d4b
remove debug message
shun159 Sep 13, 2025
ee3da8d
remove unused function
shun159 Sep 13, 2025
2528247
struct_ops: refactor kern_vdata population
shun159 Sep 13, 2025
877aac6
struct_ops: remove unused struct type
shun159 Sep 13, 2025
ad68628
struct_ops: use value type as MapSpec.Value and drop unused helpers
shun159 Sep 14, 2025
e8839fb
struct_ops: use value type as MapSpec.Value and drop unused helpers (2)
shun159 Sep 14, 2025
4adb3ac
struct_ops: remove necessary function and struct type
shun159 Sep 14, 2025
3040131
struct_ops: remove an unecessary struct field
shun159 Sep 14, 2025
fbc36d3
struct_ops: remove ad-hoc helpers and use findTargetInKernel
shun159 Sep 14, 2025
c264d20
struct_ops: make anonymous an unused variable
shun159 Sep 14, 2025
bc72363
struct_ops: we don't use MapSpec.Contents in struct_ops
shun159 Sep 14, 2025
c1aca89
struct_ops: remove AttachTo guard
shun159 Sep 14, 2025
f5f64b2
struct_ops: fix to use cached BTF for find struct_ops struct
shun159 Sep 17, 2025
3837f4b
prog: fix to ignore AttachTo string which is not matched with the
shun159 Sep 17, 2025
1c82f84
struct_ops: remove an unused helper
shun159 Sep 17, 2025
4d8b42c
collection: remove an unnecessary initializer
shun159 Sep 17, 2025
7989861
struct_ops: simplify inner struct handling, and some tweaks
shun159 Sep 18, 2025
e7994d6
struct_ops: rename populateFuncPtr to populateStructOpsProgram
shun159 Sep 18, 2025
0d78137
sturct_ops: fix to reuse BTF handle resolved by MarshalMapKV
shun159 Sep 18, 2025
cfeae69
struct_ops: add requireTestmodOps
shun159 Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 135 additions & 1 deletion collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"path/filepath"
"reflect"
"runtime"
"strings"

"github.com/cilium/ebpf/asm"
Expand Down Expand Up @@ -512,7 +513,7 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
return m, nil
}

m, err := newMapWithOptions(mapSpec, cl.opts.Maps)
m, err := newMapWithOptions(mapSpec, cl.opts.Maps, cl.types)
if err != nil {
return nil, fmt.Errorf("map %s: %w", mapName, err)
}
Expand Down Expand Up @@ -581,6 +582,7 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
}

cl.programs[progName] = prog

return prog, nil
}

Expand Down Expand Up @@ -688,6 +690,40 @@ func (cl *collectionLoader) populateDeferredMaps() error {
}
}

if mapSpec.Type == StructOpsMap {
valueType, ok := btf.As[*btf.Struct](mapSpec.Value)
if !ok {
return fmt.Errorf("value should be a *Struct")
}

userData, ok := mapSpec.Contents[0].Value.([]byte)
if !ok {
return fmt.Errorf("value should be an array of byte")
}

target := btf.Type((*btf.Struct)(nil))
_, module, err := findTargetInKernel(valueType.Name, &target, cl.types)
if err != nil {
return fmt.Errorf("lookup value type %q: %w", valueType.Name, err)
}
defer module.Close()

vType, _ := btf.As[*btf.Struct](target)

kernVData, err := translateStructData(userData, vType)
if err != nil {
return err
}

if err := populateStructOpsPrograms(vType, kernVData, cl.programs); err != nil {
return err
}
defer runtime.KeepAlive(cl.programs)

mapSpec.Contents[0] =
MapKV{Key: uint32(0), Value: kernVData}
}

// Populate and freeze the map if specified.
if err := m.finalize(mapSpec); err != nil {
return fmt.Errorf("populating map %s: %w", mapName, err)
Expand All @@ -697,6 +733,104 @@ func (cl *collectionLoader) populateDeferredMaps() error {
return nil
}

// findInnerStruct returns the "inner" struct inside a value struct_ops type.
//
// Given a value like:
//
// struct bpf_struct_ops_bpf_testmod_ops {
// struct bpf_struct_ops_common common;
// struct bpf_testmod_ops data;
// };
//
// this function returns the *btf.Struct for "bpf_testmod_ops" along with the
// byte offset of the "data" member inside the value type.
//
// The inner struct name is derived by trimming the "bpf_struct_ops_" prefix
// from the value's name.
func findInnerStruct(vType *btf.Struct) (*btf.Struct, uint32, error) {
innerName := strings.TrimPrefix(vType.Name, structOpsValuePrefix)

for _, m := range vType.Members {
if st, ok := btf.As[*btf.Struct](m.Type); ok && st.Name == innerName {
return st, m.Offset.Bytes(), nil
}
}

return nil, 0, fmt.Errorf("inner struct %q not found in %s", innerName, vType.Name)
}

// translateStructData fills in all fields in `from` that exist in `vType` with contents of fromData.
func translateStructData(fromData []byte, vType *btf.Struct) ([]byte, error) {
vTypeData := make([]byte, int(vType.Size))

inner, innerOff, err := findInnerStruct(vType)
if err != nil {
return nil, err
}

if len(fromData) < int(inner.Size) {
return nil, fmt.Errorf("inner data too short: have %d, need %d", len(fromData), inner.Size)
}

for _, m := range inner.Members {
if m.BitfieldSize > 0 {
return nil, fmt.Errorf("bitfield %s not supported in from: %s", m.Name, inner.Name)
}

if _, isPtr := btf.As[*btf.Pointer](m.Type); isPtr {
continue // skip func ptr
}

kernSz, err := btf.Sizeof(m.Type)
if err != nil {
return nil, fmt.Errorf("failed vType resolve size of %s: %w", m.Name, err)
}

srcOff := int(m.Offset.Bytes())
dstOff := int(innerOff + m.Offset.Bytes())

if srcOff < 0 || srcOff+kernSz > len(fromData) {
return nil, fmt.Errorf("member %q: userdata is too small", m.Name)
}

if dstOff < 0 || dstOff+kernSz > len(vTypeData) {
return nil, fmt.Errorf("member %q: value type is too small", m.Name)
}

copy(vTypeData[dstOff:dstOff+kernSz], fromData[srcOff:srcOff+kernSz])
}

return vTypeData, nil
}

// populateStructOpsPrograms writes progFD into `data` at the offset
func populateStructOpsPrograms(vType *btf.Struct, data []byte, programs map[string]*Program) error {
inner, innerOff, err := findInnerStruct(vType)
if err != nil {
return err
}

for _, m := range inner.Members {
if _, isPtr := btf.As[*btf.Pointer](m.Type); !isPtr {
continue
}

p, ok := programs[m.Name]
if !ok || p == nil {
continue
}

dstOff := int(innerOff + m.Offset.Bytes())

if dstOff < 0 || dstOff+8 > len(data) {
return fmt.Errorf("member %q: kern_vdata is too small", m.Name)
}
binary.LittleEndian.PutUint64(data[dstOff:dstOff+8], uint64(p.FD()))
}

return nil
}

// resolveKconfig resolves all variables declared in .kconfig and populates
// m.Contents. Does nothing if the given m.Contents is non-empty.
func resolveKconfig(m *MapSpec) error {
Expand Down
46 changes: 46 additions & 0 deletions collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/sys"
"github.com/cilium/ebpf/internal/testutils"
"github.com/cilium/ebpf/internal/testutils/testmain"
)
Expand Down Expand Up @@ -768,3 +769,48 @@ func ExampleCollectionSpec_LoadAndAssign() {
defer objs.Program.Close()
defer objs.Map.Close()
}

func TestStructOpsMapSpecSimpleLoadAndAssign(t *testing.T) {
requireTestmodOps(t)

spec := &CollectionSpec{
Programs: map[string]*ProgramSpec{
"test_1": {
Name: "test_1",
Type: StructOps,
AttachTo: "bpf_testmod_ops:test_1",
License: "GPL",
Instructions: asm.Instructions{
asm.Mov.Imm(asm.R0, 0),
asm.Return(),
},
},
},
Maps: map[string]*MapSpec{
"testmod_ops": {
Name: "testmod_ops",
Type: StructOpsMap,
Flags: sys.BPF_F_LINK,
KeySize: 4,
ValueSize: 448,
MaxEntries: 1,
Value: &btf.Struct{Name: "bpf_struct_ops_bpf_testmod_ops"},
Contents: []MapKV{
{
Key: uint32(0),
Value: make([]byte, 448),
},
},
},
},
}

coll := mustNewCollection(t, spec, nil)
for name := range spec.Maps {
qt.Assert(t, qt.IsNotNil(coll.Maps[name]))
}

for name := range spec.Programs {
qt.Assert(t, qt.IsNotNil(coll.Programs[name]))
}
}
35 changes: 35 additions & 0 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ var haveTestmod = sync.OnceValues(func() (bool, error) {
return testmod != nil, nil
})

var haveTestmodOps = sync.OnceValues(func() (bool, error) {
haveTestMod, err := haveTestmod()
if err != nil {
return false, err
}
if !haveTestMod {
return false, nil
}

target := btf.Type((*btf.Struct)(nil))
_, module, err := findTargetInKernel("bpf_struct_ops_bpf_testmod_ops", &target, btf.NewCache())
if err != nil && !errors.Is(err, btf.ErrNotFound) {
return false, err
}
if errors.Is(err, btf.ErrNotFound) {
return false, nil
}
defer module.Close()

return true, nil
})

func requireTestmod(tb testing.TB) {
tb.Helper()

Expand All @@ -44,6 +66,19 @@ func requireTestmod(tb testing.TB) {
}
}

func requireTestmodOps(tb testing.TB) {
tb.Helper()

testutils.SkipOnOldKernel(tb, "5.11", "bpf_testmod")
testmodOps, err := haveTestmodOps()
if err != nil {
tb.Fatal(err)
}
if !testmodOps {
tb.Skip("bpf_testmod_ops not loaded")
}
}

func newMap(tb testing.TB, spec *MapSpec, opts *MapOptions) (*Map, error) {
tb.Helper()

Expand Down
65 changes: 55 additions & 10 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ func NewMap(spec *MapSpec) (*Map, error) {
//
// May return an error wrapping ErrMapIncompatible.
func NewMapWithOptions(spec *MapSpec, opts MapOptions) (*Map, error) {
m, err := newMapWithOptions(spec, opts)
m, err := newMapWithOptions(spec, opts, btf.NewCache())
if err != nil {
return nil, fmt.Errorf("creating map: %w", err)
}
Expand All @@ -347,7 +347,7 @@ func NewMapWithOptions(spec *MapSpec, opts MapOptions) (*Map, error) {
return m, nil
}

func newMapWithOptions(spec *MapSpec, opts MapOptions) (_ *Map, err error) {
func newMapWithOptions(spec *MapSpec, opts MapOptions, c *btf.Cache) (_ *Map, err error) {
closeOnError := func(c io.Closer) {
if err != nil {
c.Close()
Expand Down Expand Up @@ -397,7 +397,7 @@ func newMapWithOptions(spec *MapSpec, opts MapOptions) (_ *Map, err error) {
return nil, errors.New("inner maps cannot be pinned")
}

template, err := spec.InnerMap.createMap(nil)
template, err := spec.InnerMap.createMap(nil, c)
if err != nil {
return nil, fmt.Errorf("inner map: %w", err)
}
Expand All @@ -409,7 +409,7 @@ func newMapWithOptions(spec *MapSpec, opts MapOptions) (_ *Map, err error) {
innerFd = template.fd
}

m, err := spec.createMap(innerFd)
m, err := spec.createMap(innerFd, c)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -504,7 +504,7 @@ func (m *Map) memorySize() (int, error) {

// createMap validates the spec's properties and creates the map in the kernel
// using the given opts. It does not populate or freeze the map.
func (spec *MapSpec) createMap(inner *sys.FD) (_ *Map, err error) {
func (spec *MapSpec) createMap(inner *sys.FD, c *btf.Cache) (_ *Map, err error) {
closeOnError := func(closer io.Closer) {
if err != nil {
closer.Close()
Expand Down Expand Up @@ -551,14 +551,59 @@ func (spec *MapSpec) createMap(inner *sys.FD) (_ *Map, err error) {
if err != nil && !errors.Is(err, btf.ErrNotSupported) {
return nil, fmt.Errorf("load BTF: %w", err)
}
defer handle.Close()

if handle != nil {
defer handle.Close()
if spec.Type == StructOpsMap {
if spec.Value == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about spec.Key? We should make sure that has a sensible value as well.

return nil, fmt.Errorf("struct_ops map: missing value type information")
}

valueType, ok := btf.As[*btf.Struct](spec.Value)
if !ok {
return nil, fmt.Errorf("value must be Struct type")
}

if spec.KeySize != 4 {
return nil, fmt.Errorf("struct_ops: KeySize must be 4")
}

// struct_ops: resolve value type ("bpf_struct_ops_<name>") and
// record kernel-specific BTF IDs / FDs needed for map creation.
target := btf.Type((*btf.Struct)(nil))
s, module, err := findTargetInKernel(valueType.Name, &target, c)
if err != nil {
return nil, fmt.Errorf("lookup value type %q: %w", valueType.Name, err)
}
defer module.Close()

vType := target.(*btf.Struct)

// Use BTF k/v during map creation.
btfValueTypeId, err := s.TypeID(vType)
if err != nil {
return nil, fmt.Errorf("lookup type_id: %w", err)
}

attr.ValueSize = spec.ValueSize
attr.BtfVmlinuxValueTypeId = btfValueTypeId

if handle == nil {
return nil, fmt.Errorf("struct_ops: BTF handle is not resolved")
}
attr.BtfFd = uint32(handle.FD())
attr.BtfKeyTypeId = keyTypeID
attr.BtfValueTypeId = valueTypeID

if module != nil {
// BPF_F_VTYPE_BTF_OBJ_FD is required if the type comes from a module
attr.MapFlags |= sys.BPF_F_VTYPE_BTF_OBJ_FD
// set FD for the kernel module
attr.ValueTypeBtfObjFd = int32(module.FD())
}
} else {
if handle != nil {
// Use BTF k/v during map creation.
attr.BtfFd = uint32(handle.FD())
attr.BtfKeyTypeId = keyTypeID
attr.BtfValueTypeId = valueTypeID
}
}
}

Expand Down
Loading
Loading