Skip to content

Commit f55c4e4

Browse files
committed
collection: add Variable API for accessing global BPF vars without syscalls
Signed-off-by: Timo Beckers <timo@isovalent.com>
1 parent 5e019f2 commit f55c4e4

3 files changed

Lines changed: 281 additions & 5 deletions

File tree

collection.go

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/cilium/ebpf/internal/kallsyms"
1414
"github.com/cilium/ebpf/internal/kconfig"
1515
"github.com/cilium/ebpf/internal/linux"
16+
"github.com/cilium/ebpf/internal/sys"
1617
)
1718

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

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

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

316325
return nil
317326
}
318327

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

326339
// NewCollection creates a Collection from the given spec, creating and
@@ -361,19 +374,31 @@ func NewCollectionWithOptions(spec *CollectionSpec, opts CollectionOptions) (*Co
361374
}
362375
}
363376

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

370-
// Prevent loader.cleanup from closing maps and programs.
371-
maps, progs := loader.maps, loader.programs
372-
loader.maps, loader.programs = nil, nil
394+
// Prevent loader.cleanup from closing maps, programs and vars.
395+
maps, progs, vars := loader.maps, loader.programs, loader.vars
396+
loader.maps, loader.programs, loader.vars = nil, nil, nil
373397

374398
return &Collection{
375399
progs,
376400
maps,
401+
vars,
377402
}, nil
378403
}
379404

@@ -382,6 +407,7 @@ type collectionLoader struct {
382407
opts *CollectionOptions
383408
maps map[string]*Map
384409
programs map[string]*Program
410+
vars map[string]*Variable
385411
}
386412

387413
func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collectionLoader, error) {
@@ -410,6 +436,7 @@ func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collec
410436
opts,
411437
make(map[string]*Map),
412438
make(map[string]*Program),
439+
make(map[string]*Variable),
413440
}, nil
414441
}
415442

@@ -483,6 +510,13 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
483510
return m, nil
484511
}
485512

513+
// Defer setting the mmapable flag on maps until load time. This avoids the
514+
// MapSpec having different flags on some kernel versions. Also avoid running
515+
// syscalls during ELF loading, so platforms like wasm can also parse an ELF.
516+
if isDataSection(mapSpec.Name) && haveMmapableMaps() == nil {
517+
mapSpec.Flags |= sys.BPF_F_MMAPABLE
518+
}
519+
486520
m, err := newMapWithOptions(mapSpec, cl.opts.Maps)
487521
if err != nil {
488522
return nil, fmt.Errorf("map %s: %w", mapName, err)
@@ -554,6 +588,53 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
554588
return prog, nil
555589
}
556590

591+
func (cl *collectionLoader) loadVariable(varName string) (*Variable, error) {
592+
if v := cl.vars[varName]; v != nil {
593+
return v, nil
594+
}
595+
596+
varSpec := cl.coll.Variables[varName]
597+
if varSpec == nil {
598+
return nil, fmt.Errorf("unknown variable %s", varName)
599+
}
600+
601+
// Get the key of the VariableSpec's MapSpec in the CollectionSpec.
602+
var mapName string
603+
for n, ms := range cl.coll.Maps {
604+
if ms == varSpec.m {
605+
mapName = n
606+
break
607+
}
608+
}
609+
if mapName == "" {
610+
return nil, fmt.Errorf("variable %s: underlying MapSpec %s was removed from CollectionSpec", varName, varSpec.m.Name)
611+
}
612+
613+
m, err := cl.loadMap(mapName)
614+
if err != nil {
615+
return nil, fmt.Errorf("variable %s: %w", varName, err)
616+
}
617+
618+
// If the kernel is too old or the underlying map was created without
619+
// BPF_F_MMAPABLE, [Map.Memory] will return ErrNotSupported. In this case,
620+
// emit a Variable with a nil Memory.
621+
mm, err := m.Memory()
622+
if err != nil && !errors.Is(err, ErrNotSupported) {
623+
return nil, fmt.Errorf("variable %s: getting memory for map %s: %w", varName, mapName, err)
624+
}
625+
626+
v := &Variable{
627+
varSpec.name,
628+
varSpec.offset,
629+
varSpec.size,
630+
varSpec.t,
631+
mm,
632+
}
633+
634+
cl.vars[varName] = v
635+
return v, nil
636+
}
637+
557638
// populateDeferredMaps iterates maps holding programs or other maps and loads
558639
// any dependencies. Populates all maps in cl and freezes them if specified.
559640
func (cl *collectionLoader) populateDeferredMaps() error {
@@ -740,6 +821,7 @@ func LoadCollection(file string) (*Collection, error) {
740821
func (coll *Collection) Assign(to interface{}) error {
741822
assignedMaps := make(map[string]bool)
742823
assignedProgs := make(map[string]bool)
824+
assignedVars := make(map[string]bool)
743825

744826
// Assign() only transfers already-loaded Maps and Programs. No extra
745827
// loading is done.
@@ -760,6 +842,13 @@ func (coll *Collection) Assign(to interface{}) error {
760842
}
761843
return nil, fmt.Errorf("missing map %q", name)
762844

845+
case reflect.TypeOf((*Variable)(nil)):
846+
if v := coll.Variables[name]; v != nil {
847+
assignedVars[name] = true
848+
return v, nil
849+
}
850+
return nil, fmt.Errorf("missing variable %q", name)
851+
763852
default:
764853
return nil, fmt.Errorf("unsupported type %s", typ)
765854
}
@@ -776,6 +865,9 @@ func (coll *Collection) Assign(to interface{}) error {
776865
for m := range assignedMaps {
777866
delete(coll.Maps, m)
778867
}
868+
for s := range assignedVars {
869+
delete(coll.Variables, s)
870+
}
779871

780872
return nil
781873
}

variable.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,91 @@ 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. Operations on a Variable are
117+
// performed using direct memory access, bypassing the BPF map syscall API.
118+
//
119+
// Requires Linux 5.5 and later, or a kernel supporting BPF_F_MMAPABLE.
120+
type Variable struct {
121+
name string
122+
offset uint64
123+
size uint64
124+
t btf.Type
125+
126+
mm *Memory
127+
}
128+
129+
// Size returns the size of the variable.
130+
func (v *Variable) Size() uint64 {
131+
return v.size
132+
}
133+
134+
// Constant returns true if the Variable represents a variable that is read-only
135+
// after loading the Collection into the kernel.
136+
func (v *Variable) Constant() bool {
137+
if v.mm == nil {
138+
return true
139+
}
140+
return v.mm.Readonly()
141+
}
142+
143+
// Type returns the BTF type of the variable. It contains the [btf.Var] wrapping
144+
// the underlying variable's type.
145+
func (v *Variable) Type() btf.Type {
146+
return v.t
147+
}
148+
149+
func (v *Variable) String() string {
150+
return fmt.Sprintf("%s (type=%v)", v.name, v.t)
151+
}
152+
153+
// Set the value of the Variable to the provided input. The input must marshal
154+
// to the same length as the size of the Variable.
155+
func (v *Variable) Set(in any) error {
156+
if v.mm == nil {
157+
return fmt.Errorf("variable %s: direct access requires Linux 5.5 or later: %w", v.name, ErrNotSupported)
158+
}
159+
160+
if v.Constant() {
161+
return fmt.Errorf("variable %s: %w", v.name, ErrReadOnly)
162+
}
163+
164+
buf, err := sysenc.Marshal(in, int(v.size))
165+
if err != nil {
166+
return fmt.Errorf("marshaling value %s: %w", v.name, err)
167+
}
168+
169+
if int(v.offset+v.size) > v.mm.Size() {
170+
return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name)
171+
}
172+
173+
if _, err := v.mm.WriteAt(buf.Bytes(), int64(v.offset)); err != nil {
174+
return fmt.Errorf("writing value to %s: %w", v.name, err)
175+
}
176+
177+
return nil
178+
}
179+
180+
// Get writes the value of the Variable to the provided output. The output must
181+
// be a pointer to a value whose size matches the Variable.
182+
func (v *Variable) Get(out any) error {
183+
if v.mm == nil {
184+
return fmt.Errorf("variable %s: direct access requires Linux 5.5 or later: %w", v.name, ErrNotSupported)
185+
}
186+
187+
if int(v.offset+v.size) > v.mm.Size() {
188+
return fmt.Errorf("offset %d(+%d) for variable %s is out of bounds", v.offset, v.size, v.name)
189+
}
190+
191+
b := make([]byte, v.size)
192+
if _, err := v.mm.ReadAt(b, int64(v.offset)); err != nil {
193+
return fmt.Errorf("reading value from %s: %w", v.name, err)
194+
}
195+
196+
if err := sysenc.Unmarshal(out, b); err != nil {
197+
return fmt.Errorf("unmarshaling value: %w", err)
198+
}
199+
200+
return nil
201+
}

0 commit comments

Comments
 (0)