Skip to content
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

map: check MapReplacements compatibility after applying BPF_F_MMAPABLE #1655

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 17 additions & 14 deletions collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,15 +411,10 @@ func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collec
}

// Check for existing MapSpecs in the CollectionSpec for all provided replacement maps.
for name, m := range opts.MapReplacements {
spec, ok := coll.Maps[name]
if !ok {
for name := range opts.MapReplacements {
if _, ok := coll.Maps[name]; !ok {
return nil, fmt.Errorf("replacement map %s not found in CollectionSpec", name)
}

if err := spec.Compatible(m); err != nil {
return nil, fmt.Errorf("using replacement map %s: %w", spec.Name, err)
}
}

if err := populateKallsyms(coll.Programs); err != nil {
Expand Down Expand Up @@ -494,7 +489,22 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
return nil, fmt.Errorf("missing map %s", mapName)
}

mapSpec = mapSpec.Copy()

// Defer setting the mmapable flag on maps until load time. This avoids the
// MapSpec having different flags on some kernel versions. Also avoid running
// syscalls during ELF loading, so platforms like wasm can also parse an ELF.
if isDataSection(mapSpec.Name) && haveMmapableMaps() == nil {
mapSpec.Flags |= sys.BPF_F_MMAPABLE
}

if replaceMap, ok := cl.opts.MapReplacements[mapName]; ok {
// Check compatibility with the replacement map after setting
// feature-dependent map flags.
if err := mapSpec.Compatible(replaceMap); err != nil {
return nil, fmt.Errorf("using replacement map %s: %w", mapSpec.Name, err)
}

// Clone the map to avoid closing user's map later on.
m, err := replaceMap.Clone()
if err != nil {
Expand All @@ -505,13 +515,6 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
return m, nil
}

// Defer setting the mmapable flag on maps until load time. This avoids the
// MapSpec having different flags on some kernel versions. Also avoid running
// syscalls during ELF loading, so platforms like wasm can also parse an ELF.
if isDataSection(mapSpec.Name) && haveMmapableMaps() == nil {
mapSpec.Flags |= sys.BPF_F_MMAPABLE
}

m, err := newMapWithOptions(mapSpec, cl.opts.Maps)
if err != nil {
return nil, fmt.Errorf("map %s: %w", mapName, err)
Expand Down
57 changes: 57 additions & 0 deletions collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"

"github.com/go-quicktest/qt"
"github.com/google/go-cmp/cmp"

"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/btf"
Expand Down Expand Up @@ -119,6 +120,29 @@ func TestCollectionSpecLoadCopy(t *testing.T) {
defer objs.Prog.Close()
}

func TestCollectionSpecLoadMutate(t *testing.T) {
file := testutils.NativeFile(t, "testdata/loader-%s.elf")
spec, err := LoadCollectionSpec(file)
if err != nil {
t.Fatal(err)
}

orig := spec.Copy()

var objs struct {
Prog *Program `ebpf:"xdp_prog"`
}

err = spec.LoadAndAssign(&objs, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
defer objs.Prog.Close()

if diff := cmp.Diff(orig, spec, csCmpOpts...); diff != "" {
t.Errorf("CollectionSpec was mutated after loading (-want +got):\n%s", diff)
}
}

func TestCollectionSpecRewriteMaps(t *testing.T) {
insns := asm.Instructions{
// R1 map
Expand Down Expand Up @@ -264,6 +288,7 @@ func TestCollectionSpecMapReplacements(t *testing.T) {
t.Fatalf("failed to update replaced map: %s", err)
}
}

func TestCollectionSpecMapReplacements_NonExistingMap(t *testing.T) {
cs := &CollectionSpec{
Maps: map[string]*MapSpec{
Expand Down Expand Up @@ -333,6 +358,38 @@ func TestCollectionSpecMapReplacements_SpecMismatch(t *testing.T) {
}
}

func TestMapReplacementsDataSections(t *testing.T) {
// In some circumstances, it can be useful to share data sections between
// Collections, for example to hold a ready/pause flag or some metrics.
// Test read-only maps for good measure.
file := testutils.NativeFile(t, "testdata/loader-%s.elf")
spec, err := LoadCollectionSpec(file)
if err != nil {
t.Fatal(err)
}

var objs struct {
Data *Map `ebpf:".data"`
ROData *Map `ebpf:".rodata"`
}

err = spec.LoadAndAssign(&objs, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
defer objs.Data.Close()
defer objs.ROData.Close()

qt.Assert(t, qt.IsNil(
spec.LoadAndAssign(&objs, &CollectionOptions{
MapReplacements: map[string]*Map{
".data": objs.Data,
".rodata": objs.ROData,
},
})))
defer objs.Data.Close()
defer objs.ROData.Close()
}

func TestCollectionSpec_LoadAndAssign_LazyLoading(t *testing.T) {
spec := &CollectionSpec{
Maps: map[string]*MapSpec{
Expand Down
44 changes: 22 additions & 22 deletions elf_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ import (
"github.com/go-quicktest/qt"
)

var csCmpOpts = cmp.Options{
// Dummy Comparer that works with empty readers to support test cases.
cmp.Comparer(func(a, b bytes.Reader) bool {
if a.Len() == 0 && b.Len() == 0 {
return true
}
return false
}),
cmp.Comparer(func(a, b *VariableSpec) bool {
if a.name != b.name || a.offset != b.offset || a.size != b.size {
return false
}
return true
}),
cmpopts.IgnoreTypes(btf.Spec{}),
cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder", "Types"),
cmpopts.IgnoreFields(ProgramSpec{}, "Instructions", "ByteOrder"),
cmpopts.IgnoreFields(MapSpec{}, "Key", "Value", "Contents"),
cmpopts.IgnoreUnexported(ProgramSpec{}),
}

func TestLoadCollectionSpec(t *testing.T) {
coll := &CollectionSpec{
Maps: map[string]*MapSpec{
Expand Down Expand Up @@ -198,27 +219,6 @@ func TestLoadCollectionSpec(t *testing.T) {
},
}

cmpOpts := cmp.Options{
// Dummy Comparer that works with empty readers to support test cases.
cmp.Comparer(func(a, b bytes.Reader) bool {
if a.Len() == 0 && b.Len() == 0 {
return true
}
return false
}),
cmp.Comparer(func(a, b *VariableSpec) bool {
if a.name != b.name || a.offset != b.offset || a.size != b.size {
return false
}
return true
}),
cmpopts.IgnoreTypes(new(btf.Spec)),
cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder", "Types"),
cmpopts.IgnoreFields(ProgramSpec{}, "Instructions", "ByteOrder"),
cmpopts.IgnoreFields(MapSpec{}, "Key", "Value", "Contents"),
cmpopts.IgnoreUnexported(ProgramSpec{}),
}

testutils.Files(t, testutils.Glob(t, "testdata/loader-*.elf"), func(t *testing.T, file string) {
have, err := LoadCollectionSpec(file)
if err != nil {
Expand Down Expand Up @@ -250,7 +250,7 @@ func TestLoadCollectionSpec(t *testing.T) {
qt.Assert(t, qt.Equals(have.Maps["perf_event_array"].ValueSize, 0))
qt.Assert(t, qt.IsNotNil(have.Maps["perf_event_array"].Value))

if diff := cmp.Diff(coll, have, cmpOpts...); diff != "" {
if diff := cmp.Diff(coll, have, csCmpOpts); diff != "" {
t.Errorf("MapSpec mismatch (-want +got):\n%s", diff)
}

Expand Down
Loading