Skip to content

Commit

Permalink
collection: add Variable API for accessing global BPF vars without sy…
Browse files Browse the repository at this point in the history
…scalls

Signed-off-by: Timo Beckers <[email protected]>
  • Loading branch information
ti-mo committed Oct 3, 2024
1 parent e16efbb commit b9faaef
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 5 deletions.
99 changes: 94 additions & 5 deletions collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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) {
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down
77 changes: 77 additions & 0 deletions variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
74 changes: 74 additions & 0 deletions variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/go-quicktest/qt"

"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/testutils"
)

Expand Down Expand Up @@ -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))
}

0 comments on commit b9faaef

Please sign in to comment.