Skip to content

Commit 80e83a4

Browse files
committed
collection: add Variable API for accessing global BPF vars without syscalls
Signed-off-by: Timo Beckers <[email protected]>
1 parent 4290021 commit 80e83a4

File tree

3 files changed

+247
-5
lines changed

3 files changed

+247
-5
lines changed

collection.go

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/cilium/ebpf/internal"
1313
"github.com/cilium/ebpf/internal/kconfig"
1414
"github.com/cilium/ebpf/internal/linux"
15+
"github.com/cilium/ebpf/internal/sys"
1516
)
1617

1718
// CollectionOptions control loading a collection into the kernel.
@@ -259,6 +260,7 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions)
259260
// Support assigning Programs and Maps, lazy-loading the required objects.
260261
assignedMaps := make(map[string]bool)
261262
assignedProgs := make(map[string]bool)
263+
assignedVars := make(map[string]bool)
262264

263265
getValue := func(typ reflect.Type, name string) (interface{}, error) {
264266
switch typ {
@@ -271,6 +273,10 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions)
271273
assignedMaps[name] = true
272274
return loader.loadMap(name)
273275

276+
case reflect.TypeOf((*Variable)(nil)):
277+
assignedVars[name] = true
278+
return loader.loadVariable(name)
279+
274280
default:
275281
return nil, fmt.Errorf("unsupported type %s", typ)
276282
}
@@ -311,15 +317,22 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions)
311317
for p := range assignedProgs {
312318
delete(loader.programs, p)
313319
}
320+
for p := range assignedVars {
321+
delete(loader.vars, p)
322+
}
314323

315324
return nil
316325
}
317326

318-
// Collection is a collection of Programs and Maps associated
319-
// with their symbols
327+
// Collection is a collection of live BPF resources present in the kernel.
320328
type Collection struct {
321329
Programs map[string]*Program
322330
Maps map[string]*Map
331+
332+
// Variables contains global variables used by the Collection's program(s).
333+
// Only populated on Linux 5.5 and later or on kernels supporting
334+
// BPF_F_MMAPABLE.
335+
Variables map[string]*Variable
323336
}
324337

325338
// NewCollection creates a Collection from the given spec, creating and
@@ -360,19 +373,31 @@ func NewCollectionWithOptions(spec *CollectionSpec, opts CollectionOptions) (*Co
360373
}
361374
}
362375

376+
for varName := range spec.Variables {
377+
_, err := loader.loadVariable(varName)
378+
if errors.Is(err, ErrNotSupported) {
379+
// Don't emit Variable if the kernel lacks support for mmapable maps.
380+
continue
381+
}
382+
if err != nil {
383+
return nil, err
384+
}
385+
}
386+
363387
// Maps can contain Program and Map stubs, so populate them after
364388
// all Maps and Programs have been successfully loaded.
365389
if err := loader.populateDeferredMaps(); err != nil {
366390
return nil, err
367391
}
368392

369-
// Prevent loader.cleanup from closing maps and programs.
370-
maps, progs := loader.maps, loader.programs
371-
loader.maps, loader.programs = nil, nil
393+
// Prevent loader.cleanup from closing maps, programs and vars.
394+
maps, progs, vars := loader.maps, loader.programs, loader.vars
395+
loader.maps, loader.programs, loader.vars = nil, nil, nil
372396

373397
return &Collection{
374398
progs,
375399
maps,
400+
vars,
376401
}, nil
377402
}
378403

@@ -381,6 +406,7 @@ type collectionLoader struct {
381406
opts *CollectionOptions
382407
maps map[string]*Map
383408
programs map[string]*Program
409+
vars map[string]*Variable
384410
}
385411

386412
func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collectionLoader, error) {
@@ -405,6 +431,7 @@ func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collec
405431
opts,
406432
make(map[string]*Map),
407433
make(map[string]*Program),
434+
make(map[string]*Variable),
408435
}, nil
409436
}
410437

@@ -439,6 +466,13 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
439466
return m, nil
440467
}
441468

469+
// Defer setting the mmapable flag on maps until load time. This avoids the
470+
// MapSpec having different flags on some kernel versions. Also avoid running
471+
// syscalls during ELF loading, so platforms like wasm can also parse an ELF.
472+
if isDataSection(mapSpec.Name) && haveMmapableMaps() == nil {
473+
mapSpec.Flags |= sys.BPF_F_MMAPABLE
474+
}
475+
442476
m, err := newMapWithOptions(mapSpec, cl.opts.Maps)
443477
if err != nil {
444478
return nil, fmt.Errorf("map %s: %w", mapName, err)
@@ -510,6 +544,50 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
510544
return prog, nil
511545
}
512546

547+
func (cl *collectionLoader) loadVariable(varName string) (*Variable, error) {
548+
if v := cl.vars[varName]; v != nil {
549+
return v, nil
550+
}
551+
552+
varSpec := cl.coll.Variables[varName]
553+
if varSpec == nil {
554+
return nil, fmt.Errorf("unknown variable %s", varName)
555+
}
556+
557+
// Get the key of the VariableSpec's MapSpec in the CollectionSpec.
558+
var mapName string
559+
for n, ms := range cl.coll.Maps {
560+
if ms == varSpec.m {
561+
mapName = n
562+
break
563+
}
564+
}
565+
if mapName == "" {
566+
return nil, fmt.Errorf("variable %s: underlying MapSpec %s was removed from CollectionSpec", varName, varSpec.m.Name)
567+
}
568+
569+
m, err := cl.loadMap(mapName)
570+
if err != nil {
571+
return nil, fmt.Errorf("variable %s: %w", varName, err)
572+
}
573+
574+
mm, err := m.Memory()
575+
if err != nil {
576+
return nil, fmt.Errorf("variable %s: getting memory of map %s: %w", varName, mapName, err)
577+
}
578+
579+
v := &Variable{
580+
varSpec.name,
581+
varSpec.offset,
582+
varSpec.size,
583+
mm,
584+
varSpec.t,
585+
}
586+
587+
cl.vars[varName] = v
588+
return v, nil
589+
}
590+
513591
// populateDeferredMaps iterates maps holding programs or other maps and loads
514592
// any dependencies. Populates all maps in cl and freezes them if specified.
515593
func (cl *collectionLoader) populateDeferredMaps() error {
@@ -696,6 +774,7 @@ func LoadCollection(file string) (*Collection, error) {
696774
func (coll *Collection) Assign(to interface{}) error {
697775
assignedMaps := make(map[string]bool)
698776
assignedProgs := make(map[string]bool)
777+
assignedVars := make(map[string]bool)
699778

700779
// Assign() only transfers already-loaded Maps and Programs. No extra
701780
// loading is done.
@@ -716,6 +795,13 @@ func (coll *Collection) Assign(to interface{}) error {
716795
}
717796
return nil, fmt.Errorf("missing map %q", name)
718797

798+
case reflect.TypeOf((*Variable)(nil)):
799+
if v := coll.Variables[name]; v != nil {
800+
assignedVars[name] = true
801+
return v, nil
802+
}
803+
return nil, fmt.Errorf("missing variable %q", name)
804+
719805
default:
720806
return nil, fmt.Errorf("unsupported type %s", typ)
721807
}
@@ -732,6 +818,9 @@ func (coll *Collection) Assign(to interface{}) error {
732818
for m := range assignedMaps {
733819
delete(coll.Maps, m)
734820
}
821+
for s := range assignedVars {
822+
delete(coll.Variables, s)
823+
}
735824

736825
return nil
737826
}

variable.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,82 @@ func (s *VariableSpec) copy(cpy *CollectionSpec) *VariableSpec {
111111

112112
return nil
113113
}
114+
115+
// Variable is a convenience wrapper for modifying global variables of a
116+
// Collection after loading it into the kernel.
117+
//
118+
// Operations on a Variable's underlying Map are performed in the host's native
119+
// endianness and using direct memory access, bypassing the BPF map syscall API.
120+
// As such, Variables are only supported on Linux 5.5 and later or on kernels
121+
// supporting BPF_F_MMAPABLE.
122+
type Variable struct {
123+
name string
124+
offset uint64
125+
size uint64
126+
127+
mm *Memory
128+
t btf.Type
129+
}
130+
131+
// Size returns the size of the variable.
132+
func (v *Variable) Size() uint64 {
133+
return v.size
134+
}
135+
136+
// Constant returns true if the Variable represents a variable that is read-only
137+
// after loading the Collection into the kernel.
138+
func (v *Variable) Constant() bool {
139+
return v.mm.Readonly()
140+
}
141+
142+
// Type returns the BTF type of the variable. It contains the [btf.Var] wrapping
143+
// the underlying variable's type.
144+
func (v *Variable) Type() btf.Type {
145+
return v.t
146+
}
147+
148+
func (v *Variable) String() string {
149+
return fmt.Sprintf("%s (type=%v)", v.name, v.t)
150+
}
151+
152+
// Set the value of the Variable to the provided input. The input must marshal
153+
// to the same length as the size of the Variable.
154+
func (v *Variable) Set(in any) error {
155+
if v.Constant() {
156+
return fmt.Errorf("variable %s: %w", v.name, ErrReadOnly)
157+
}
158+
159+
buf, err := sysenc.Marshal(in, int(v.size))
160+
if err != nil {
161+
return fmt.Errorf("marshaling value %s: %w", v.name, err)
162+
}
163+
164+
if int(v.offset+v.size) > v.mm.Size() {
165+
return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name)
166+
}
167+
168+
if _, err := v.mm.WriteAt(buf.Bytes(), int64(v.offset)); err != nil {
169+
return fmt.Errorf("writing value to %s: %w", v.name, err)
170+
}
171+
172+
return nil
173+
}
174+
175+
// Get writes the value of the Variable to the provided output. The output must
176+
// be a pointer to a value whose size matches the Variable.
177+
func (v *Variable) Get(out any) error {
178+
if int(v.offset+v.size) > v.mm.Size() {
179+
return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name)
180+
}
181+
182+
b := make([]byte, v.size)
183+
if _, err := v.mm.ReadAt(b, int64(v.offset)); err != nil {
184+
return fmt.Errorf("reading value from %s: %w", v.name, err)
185+
}
186+
187+
if err := sysenc.Unmarshal(out, b); err != nil {
188+
return fmt.Errorf("unmarshaling value: %w", err)
189+
}
190+
191+
return nil
192+
}

variable_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/go-quicktest/qt"
77

8+
"github.com/cilium/ebpf/internal"
89
"github.com/cilium/ebpf/internal/testutils"
910
)
1011

@@ -62,3 +63,76 @@ func TestVariableSpecCopy(t *testing.T) {
6263
zero := make([]byte, 4)
6364
qt.Assert(t, qt.DeepEquals(spec.Maps[".rodata"].Contents[0].Value.([]byte), zero))
6465
}
66+
67+
func mustReturn(tb testing.TB, prog *Program, value uint32) {
68+
tb.Helper()
69+
70+
ret, _, err := prog.Test(internal.EmptyBPFContext)
71+
qt.Assert(tb, qt.IsNil(err))
72+
qt.Assert(tb, qt.Equals(ret, value))
73+
}
74+
75+
func TestVariable(t *testing.T) {
76+
testutils.SkipOnOldKernel(t, "5.5", "mmapable maps")
77+
78+
file := testutils.NativeFile(t, "testdata/variables-%s.elf")
79+
spec, err := LoadCollectionSpec(file)
80+
qt.Assert(t, qt.IsNil(err))
81+
82+
obj := struct {
83+
GetBSS *Program `ebpf:"get_bss"`
84+
GetData *Program `ebpf:"get_data"`
85+
CheckStruct *Program `ebpf:"check_struct"`
86+
87+
BSS *Variable `ebpf:"var_bss"`
88+
Data *Variable `ebpf:"var_data"`
89+
Struct *Variable `ebpf:"var_struct"`
90+
}{}
91+
92+
qt.Assert(t, qt.IsNil(spec.LoadAndAssign(&obj, nil)))
93+
t.Cleanup(func() {
94+
obj.GetBSS.Close()
95+
obj.GetData.Close()
96+
obj.CheckStruct.Close()
97+
})
98+
99+
mustReturn(t, obj.GetBSS, 0)
100+
mustReturn(t, obj.GetData, 0)
101+
mustReturn(t, obj.CheckStruct, 0)
102+
103+
want := uint32(4242424242)
104+
qt.Assert(t, qt.IsNil(obj.BSS.Set(want)))
105+
mustReturn(t, obj.GetBSS, want)
106+
qt.Assert(t, qt.IsNil(obj.Data.Set(want)))
107+
mustReturn(t, obj.GetData, want)
108+
qt.Assert(t, qt.IsNil(obj.Struct.Set(&struct{ A, B uint64 }{0xa, 0xb})))
109+
mustReturn(t, obj.CheckStruct, 1)
110+
}
111+
112+
func TestVariableConst(t *testing.T) {
113+
testutils.SkipOnOldKernel(t, "5.5", "mmapable maps")
114+
115+
file := testutils.NativeFile(t, "testdata/variables-%s.elf")
116+
spec, err := LoadCollectionSpec(file)
117+
qt.Assert(t, qt.IsNil(err))
118+
119+
want := uint32(12345)
120+
qt.Assert(t, qt.IsNil(spec.Variables["var_rodata"].Set(want)))
121+
122+
obj := struct {
123+
GetRodata *Program `ebpf:"get_rodata"`
124+
Rodata *Variable `ebpf:"var_rodata"`
125+
}{}
126+
127+
qt.Assert(t, qt.IsNil(spec.LoadAndAssign(&obj, nil)))
128+
t.Cleanup(func() {
129+
obj.GetRodata.Close()
130+
})
131+
132+
var got uint32
133+
qt.Assert(t, qt.IsNil(obj.Rodata.Get(&got)))
134+
qt.Assert(t, qt.Equals(got, want))
135+
mustReturn(t, obj.GetRodata, want)
136+
137+
qt.Assert(t, qt.ErrorIs(obj.Rodata.Set(want), ErrReadOnly))
138+
}

0 commit comments

Comments
 (0)