From b9faaeff7663cc6ab5b3b68c3edb74614cc59785 Mon Sep 17 00:00:00 2001 From: Timo Beckers Date: Tue, 1 Oct 2024 13:37:24 +0200 Subject: [PATCH] collection: add Variable API for accessing global BPF vars without syscalls Signed-off-by: Timo Beckers --- collection.go | 99 +++++++++++++++++++++++++++++++++++++++++++++--- variable.go | 77 +++++++++++++++++++++++++++++++++++++ variable_test.go | 74 ++++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 5 deletions(-) diff --git a/collection.go b/collection.go index 0512d10f2..9298fe1d9 100644 --- a/collection.go +++ b/collection.go @@ -12,6 +12,7 @@ import ( "github.com/cilium/ebpf/internal" "github.com/cilium/ebpf/internal/kconfig" "github.com/cilium/ebpf/internal/linux" + "github.com/cilium/ebpf/internal/sys" ) // CollectionOptions control loading a collection into the kernel. @@ -259,6 +260,7 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) // Support assigning Programs and Maps, lazy-loading the required objects. assignedMaps := make(map[string]bool) assignedProgs := make(map[string]bool) + assignedVars := make(map[string]bool) getValue := func(typ reflect.Type, name string) (interface{}, error) { switch typ { @@ -271,6 +273,10 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) assignedMaps[name] = true return loader.loadMap(name) + case reflect.TypeOf((*Variable)(nil)): + assignedVars[name] = true + return loader.loadVariable(name) + default: return nil, fmt.Errorf("unsupported type %s", typ) } @@ -311,15 +317,22 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) for p := range assignedProgs { delete(loader.programs, p) } + for p := range assignedVars { + delete(loader.vars, p) + } return nil } -// Collection is a collection of Programs and Maps associated -// with their symbols +// Collection is a collection of live BPF resources present in the kernel. type Collection struct { Programs map[string]*Program Maps map[string]*Map + + // Variables contains global variables used by the Collection's program(s). + // Only populated on Linux 5.5 and later or on kernels supporting + // BPF_F_MMAPABLE. + Variables map[string]*Variable } // NewCollection creates a Collection from the given spec, creating and @@ -360,19 +373,31 @@ func NewCollectionWithOptions(spec *CollectionSpec, opts CollectionOptions) (*Co } } + for varName := range spec.Variables { + _, err := loader.loadVariable(varName) + if errors.Is(err, ErrNotSupported) { + // Don't emit Variable if the kernel lacks support for mmapable maps. + continue + } + if err != nil { + return nil, err + } + } + // Maps can contain Program and Map stubs, so populate them after // all Maps and Programs have been successfully loaded. if err := loader.populateDeferredMaps(); err != nil { return nil, err } - // Prevent loader.cleanup from closing maps and programs. - maps, progs := loader.maps, loader.programs - loader.maps, loader.programs = nil, nil + // Prevent loader.cleanup from closing maps, programs and vars. + maps, progs, vars := loader.maps, loader.programs, loader.vars + loader.maps, loader.programs, loader.vars = nil, nil, nil return &Collection{ progs, maps, + vars, }, nil } @@ -381,6 +406,7 @@ type collectionLoader struct { opts *CollectionOptions maps map[string]*Map programs map[string]*Program + vars map[string]*Variable } func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collectionLoader, error) { @@ -405,6 +431,7 @@ func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collec opts, make(map[string]*Map), make(map[string]*Program), + make(map[string]*Variable), }, nil } @@ -439,6 +466,13 @@ 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) @@ -510,6 +544,50 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) { return prog, nil } +func (cl *collectionLoader) loadVariable(varName string) (*Variable, error) { + if v := cl.vars[varName]; v != nil { + return v, nil + } + + varSpec := cl.coll.Variables[varName] + if varSpec == nil { + return nil, fmt.Errorf("unknown variable %s", varName) + } + + // Get the key of the VariableSpec's MapSpec in the CollectionSpec. + var mapName string + for n, ms := range cl.coll.Maps { + if ms == varSpec.m { + mapName = n + break + } + } + if mapName == "" { + return nil, fmt.Errorf("variable %s: underlying MapSpec %s was removed from CollectionSpec", varName, varSpec.m.Name) + } + + m, err := cl.loadMap(mapName) + if err != nil { + return nil, fmt.Errorf("variable %s: %w", varName, err) + } + + mm, err := m.Memory() + if err != nil { + return nil, fmt.Errorf("variable %s: getting memory of map %s: %w", varName, mapName, err) + } + + v := &Variable{ + varSpec.name, + varSpec.offset, + varSpec.size, + varSpec.t, + mm, + } + + cl.vars[varName] = v + return v, nil +} + // populateDeferredMaps iterates maps holding programs or other maps and loads // any dependencies. Populates all maps in cl and freezes them if specified. func (cl *collectionLoader) populateDeferredMaps() error { @@ -696,6 +774,7 @@ func LoadCollection(file string) (*Collection, error) { func (coll *Collection) Assign(to interface{}) error { assignedMaps := make(map[string]bool) assignedProgs := make(map[string]bool) + assignedVars := make(map[string]bool) // Assign() only transfers already-loaded Maps and Programs. No extra // loading is done. @@ -716,6 +795,13 @@ func (coll *Collection) Assign(to interface{}) error { } return nil, fmt.Errorf("missing map %q", name) + case reflect.TypeOf((*Variable)(nil)): + if v := coll.Variables[name]; v != nil { + assignedVars[name] = true + return v, nil + } + return nil, fmt.Errorf("missing variable %q", name) + default: return nil, fmt.Errorf("unsupported type %s", typ) } @@ -732,6 +818,9 @@ func (coll *Collection) Assign(to interface{}) error { for m := range assignedMaps { delete(coll.Maps, m) } + for s := range assignedVars { + delete(coll.Variables, s) + } return nil } diff --git a/variable.go b/variable.go index 916f3a5d4..4d756d883 100644 --- a/variable.go +++ b/variable.go @@ -111,3 +111,80 @@ func (s *VariableSpec) copy(cpy *CollectionSpec) *VariableSpec { return nil } + +// Variable is a convenience wrapper for modifying global variables of a +// Collection after loading it into the kernel. Operations on a Variable are +// performed using direct memory access, bypassing the BPF map syscall API. +// +// Requires Linux 5.5 and later, or a kernel supporting BPF_F_MMAPABLE. +type Variable struct { + name string + offset uint64 + size uint64 + t btf.Type + + mm *Memory +} + +// Size returns the size of the variable. +func (v *Variable) Size() uint64 { + return v.size +} + +// Constant returns true if the Variable represents a variable that is read-only +// after loading the Collection into the kernel. +func (v *Variable) Constant() bool { + return v.mm.Readonly() +} + +// Type returns the BTF type of the variable. It contains the [btf.Var] wrapping +// the underlying variable's type. +func (v *Variable) Type() btf.Type { + return v.t +} + +func (v *Variable) String() string { + return fmt.Sprintf("%s (type=%v)", v.name, v.t) +} + +// Set the value of the Variable to the provided input. The input must marshal +// to the same length as the size of the Variable. +func (v *Variable) Set(in any) error { + if v.Constant() { + return fmt.Errorf("variable %s: %w", v.name, ErrReadOnly) + } + + buf, err := sysenc.Marshal(in, int(v.size)) + if err != nil { + return fmt.Errorf("marshaling value %s: %w", v.name, err) + } + + if int(v.offset+v.size) > v.mm.Size() { + return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name) + } + + if _, err := v.mm.WriteAt(buf.Bytes(), int64(v.offset)); err != nil { + return fmt.Errorf("writing value to %s: %w", v.name, err) + } + + return nil +} + +// Get writes the value of the Variable to the provided output. The output must +// be a pointer to a value whose size matches the Variable. +func (v *Variable) Get(out any) error { + if int(v.offset+v.size) > v.mm.Size() { + return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name) + } + + b := make([]byte, v.size) + if _, err := v.mm.ReadAt(b, int64(v.offset)); err != nil { + return fmt.Errorf("reading value from %s: %w", v.name, err) + } + + if err := sysenc.Unmarshal(out, b); err != nil { + return fmt.Errorf("unmarshaling value: %w", err) + } + + return nil +} diff --git a/variable_test.go b/variable_test.go index 717db9528..4de3d9484 100644 --- a/variable_test.go +++ b/variable_test.go @@ -5,6 +5,7 @@ import ( "github.com/go-quicktest/qt" + "github.com/cilium/ebpf/internal" "github.com/cilium/ebpf/internal/testutils" ) @@ -62,3 +63,76 @@ func TestVariableSpecCopy(t *testing.T) { zero := make([]byte, 4) qt.Assert(t, qt.DeepEquals(spec.Maps[".rodata"].Contents[0].Value.([]byte), zero)) } + +func mustReturn(tb testing.TB, prog *Program, value uint32) { + tb.Helper() + + ret, _, err := prog.Test(internal.EmptyBPFContext) + qt.Assert(tb, qt.IsNil(err)) + qt.Assert(tb, qt.Equals(ret, value)) +} + +func TestVariable(t *testing.T) { + testutils.SkipOnOldKernel(t, "5.5", "mmapable maps") + + file := testutils.NativeFile(t, "testdata/variables-%s.elf") + spec, err := LoadCollectionSpec(file) + qt.Assert(t, qt.IsNil(err)) + + obj := struct { + GetBSS *Program `ebpf:"get_bss"` + GetData *Program `ebpf:"get_data"` + CheckStruct *Program `ebpf:"check_struct"` + + BSS *Variable `ebpf:"var_bss"` + Data *Variable `ebpf:"var_data"` + Struct *Variable `ebpf:"var_struct"` + }{} + + qt.Assert(t, qt.IsNil(spec.LoadAndAssign(&obj, nil))) + t.Cleanup(func() { + obj.GetBSS.Close() + obj.GetData.Close() + obj.CheckStruct.Close() + }) + + mustReturn(t, obj.GetBSS, 0) + mustReturn(t, obj.GetData, 0) + mustReturn(t, obj.CheckStruct, 0) + + want := uint32(4242424242) + qt.Assert(t, qt.IsNil(obj.BSS.Set(want))) + mustReturn(t, obj.GetBSS, want) + qt.Assert(t, qt.IsNil(obj.Data.Set(want))) + mustReturn(t, obj.GetData, want) + qt.Assert(t, qt.IsNil(obj.Struct.Set(&struct{ A, B uint64 }{0xa, 0xb}))) + mustReturn(t, obj.CheckStruct, 1) +} + +func TestVariableConst(t *testing.T) { + testutils.SkipOnOldKernel(t, "5.5", "mmapable maps") + + file := testutils.NativeFile(t, "testdata/variables-%s.elf") + spec, err := LoadCollectionSpec(file) + qt.Assert(t, qt.IsNil(err)) + + want := uint32(12345) + qt.Assert(t, qt.IsNil(spec.Variables["var_rodata"].Set(want))) + + obj := struct { + GetRodata *Program `ebpf:"get_rodata"` + Rodata *Variable `ebpf:"var_rodata"` + }{} + + qt.Assert(t, qt.IsNil(spec.LoadAndAssign(&obj, nil))) + t.Cleanup(func() { + obj.GetRodata.Close() + }) + + var got uint32 + qt.Assert(t, qt.IsNil(obj.Rodata.Get(&got))) + qt.Assert(t, qt.Equals(got, want)) + mustReturn(t, obj.GetRodata, want) + + qt.Assert(t, qt.ErrorIs(obj.Rodata.Set(want), ErrReadOnly)) +}