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

pin: new package for loading bpf pins and walking bpffs directories #1626

Merged
merged 3 commits into from
Dec 16, 2024
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
9 changes: 6 additions & 3 deletions internal/sys/fd.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,13 @@ func (fd *FD) Close() error {
return nil
}

return unix.Close(fd.disown())
return unix.Close(fd.Disown())
}

func (fd *FD) disown() int {
// Disown destroys the FD and returns its raw file descriptor without closing
ti-mo marked this conversation as resolved.
Show resolved Hide resolved
// it. After this call, the underlying fd is no longer tied to the FD's
// lifecycle.
func (fd *FD) Disown() int {
value := fd.raw
fdtrace.ForgetFD(value)
fd.raw = -1
Expand Down Expand Up @@ -118,7 +121,7 @@ func (fd *FD) File(name string) *os.File {
return nil
}

return os.NewFile(uintptr(fd.disown()), name)
return os.NewFile(uintptr(fd.Disown()), name)
}

// ObjGetTyped wraps [ObjGet] with a readlink call to extract the type of the
Expand Down
8 changes: 8 additions & 0 deletions internal/testutils/checkers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import (
"github.com/go-quicktest/qt"
)

// Contains checks if interface value I is of type T. Use with qt.Satisfies:
//
// qt.Assert(t, qt.Satisfies(p, testutils.Contains[*ebpf.Program]))
func Contains[T, I any](i I) bool {
_, ok := any(i).(T)
return ok
}

// IsDeepCopy checks that got is a deep copy of want.
//
// All primitive values must be equal, but pointers must be distinct.
Expand Down
3 changes: 3 additions & 0 deletions pin/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package pin provides utility functions for working with pinned objects on bpffs.

package pin
40 changes: 40 additions & 0 deletions pin/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package pin

import (
"fmt"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal/sys"
"github.com/cilium/ebpf/link"
)

// Pinner is an interface implemented by all eBPF objects that support pinning
// to a bpf virtual filesystem.
type Pinner interface {
Pin(string) error
}

// Load retrieves a pinned object from a bpf virtual filesystem. It returns one
// of [ebpf.Map], [ebpf.Program], or [link.Link].
//
// Trying to open anything other than a bpf object is an error.
func Load(path string, opts *ebpf.LoadPinOptions) (Pinner, error) {
fd, typ, err := sys.ObjGetTyped(&sys.ObjGetAttr{
Pathname: sys.NewStringPointer(path),
FileFlags: opts.Marshal(),
})
if err != nil {
return nil, fmt.Errorf("opening pin %s: %w", path, err)
}

switch typ {
case sys.BPF_TYPE_MAP:
return ebpf.NewMapFromFD(fd.Disown())
case sys.BPF_TYPE_PROG:
return ebpf.NewProgramFromFD(fd.Disown())
case sys.BPF_TYPE_LINK:
return link.NewFromFD(fd.Disown())
}

return nil, fmt.Errorf("unknown object type %d", typ)
}
85 changes: 85 additions & 0 deletions pin/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package pin

import (
"path/filepath"
"testing"

"github.com/go-quicktest/qt"

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

func mustPinnedProgram(t *testing.T, path string) *ebpf.Program {
t.Helper()

spec := &ebpf.ProgramSpec{
Name: "test",
Type: ebpf.SocketFilter,
Instructions: asm.Instructions{
asm.LoadImm(asm.R0, 2, asm.DWord),
asm.Return(),
},
License: "MIT",
}

p, err := ebpf.NewProgram(spec)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { p.Close() })

if err := p.Pin(path); err != nil {
t.Fatal(err)
}

return p
}

func mustPinnedMap(t *testing.T, path string) *ebpf.Map {
t.Helper()

spec := &ebpf.MapSpec{
Name: "test",
Type: ebpf.Array,
KeySize: 4,
ValueSize: 4,
MaxEntries: 1,
}

m, err := ebpf.NewMap(spec)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { m.Close() })

if err := m.Pin(path); err != nil {
t.Fatal(err)
}

return m
}

func TestLoad(t *testing.T) {
testutils.SkipOnOldKernel(t, "4.10", "reading program fdinfo")

tmp := testutils.TempBPFFS(t)

mpath := filepath.Join(tmp, "map")
ppath := filepath.Join(tmp, "prog")

mustPinnedMap(t, mpath)
mustPinnedProgram(t, ppath)

_, err := Load(tmp, nil)
qt.Assert(t, qt.IsNotNil(err))

m, err := Load(mpath, nil)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Satisfies(m, testutils.Contains[*ebpf.Map]))

p, err := Load(ppath, nil)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Satisfies(p, testutils.Contains[*ebpf.Program]))
}
49 changes: 49 additions & 0 deletions pin/walk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package pin

import (
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/cilium/ebpf/internal/linux"
"github.com/cilium/ebpf/internal/unix"
)

// WalkDirFunc is the type of the function called for each object visited by
// [WalkDir]. It's identical to [fs.WalkDirFunc], but with an extra [Pinner]
// argument. If the visited node is a directory, obj is nil.
//
// err contains any errors encountered during bpffs traversal or object loading.
type WalkDirFunc func(path string, d fs.DirEntry, obj Pinner, err error) error

// WalkDir walks the file tree rooted at path, calling bpffn for each node in
// the tree, including directories. Running WalkDir on a non-bpf filesystem is
// an error. Otherwise identical in behavior to [fs.WalkDir].
//
// See the [WalkDirFunc] for more information.
func WalkDir(root string, bpffn WalkDirFunc) error {
fsType, err := linux.FSType(root)
if err != nil {
return err
}
if fsType != unix.BPF_FS_MAGIC {
return fmt.Errorf("%s is not on a bpf filesystem", root)
}

fn := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return bpffn(path, nil, nil, err)
}

if d.IsDir() {
return bpffn(path, d, nil, err)
}

obj, err := Load(filepath.Join(root, path), nil)

return bpffn(path, d, obj, err)
}

return fs.WalkDir(os.DirFS(root), ".", fn)
}
54 changes: 54 additions & 0 deletions pin/walk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package pin

import (
"io/fs"
"os"
"path/filepath"
"testing"

"github.com/go-quicktest/qt"

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

func TestWalkDir(t *testing.T) {
testutils.SkipOnOldKernel(t, "4.10", "reading program fdinfo")

tmp := testutils.TempBPFFS(t)
dir := filepath.Join(tmp, "dir")
qt.Assert(t, qt.IsNil(os.Mkdir(dir, 0755)))

mustPinnedProgram(t, filepath.Join(tmp, "pinned_prog"))
mustPinnedMap(t, filepath.Join(dir, "pinned_map"))

entries := make(map[string]string)

bpffn := func(path string, d fs.DirEntry, obj Pinner, err error) error {
qt.Assert(t, qt.IsNil(err))

if path == "." {
return nil
}

switch obj.(type) {
case *ebpf.Program:
entries[path] = "prog"
case *ebpf.Map:
entries[path] = "map"
default:
entries[path] = ""
}

return nil
}
qt.Assert(t, qt.IsNil(WalkDir(tmp, bpffn)))

qt.Assert(t, qt.DeepEquals(entries, map[string]string{
"pinned_prog": "prog",
"dir": "",
"dir/pinned_map": "map",
}))

qt.Assert(t, qt.IsNotNil(WalkDir("/", nil)))
}
Loading