Skip to content

Commit

Permalink
map,prog,link: verify object type in LoadPinned*()
Browse files Browse the repository at this point in the history
Before this patch, LoadPinned{Map,Prog,Link} all succeeded when called
on a pin of the wrong type, leading to garbage in the object's info.

This patch adds an ObjGetTyped() wrapper around ObjGet() to obtain a file
descriptor's bpf object type. Despite the warnings in
https://dxuuu.xyz/filecaps.html, this works for unprivileged processes with
file caps as well. /proc/self/fd/ contains symlinks whose underlying fds
remain accessible by the user, presumably since they're still owned by the
process owner.

Kernel patches are underway for adding a bpf_type field to ObjGetAttr
to provide future proofing in case new bpf objects are added to the
kernel. The current checks were designed to make this addition as
straightforward as possible.

Signed-off-by: Timo Beckers <timo@isovalent.com>
Co-authored-by: Mahe Tardy <mahe.tardy@gmail.com>
  • Loading branch information
ti-mo and mtardy committed Dec 12, 2024
1 parent a2f321a commit d4a40af
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 6 deletions.
42 changes: 42 additions & 0 deletions internal/sys/fd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"fmt"
"math"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"

"github.com/cilium/ebpf/internal/testutils/fdtrace"
"github.com/cilium/ebpf/internal/unix"
Expand Down Expand Up @@ -118,3 +120,43 @@ func (fd *FD) File(name string) *os.File {

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

// ObjGetTyped wraps [ObjGet] with a readlink call to extract the type of the
// underlying bpf object.
func ObjGetTyped(attr *ObjGetAttr) (*FD, ObjType, error) {
fd, err := ObjGet(attr)
if err != nil {
return nil, 0, err
}

typ, err := readType(fd)
if err != nil {
_ = fd.Close()
return nil, 0, fmt.Errorf("reading fd type: %w", err)
}

return fd, typ, nil
}

// readType returns the bpf object type of the file descriptor by calling
// readlink(3). Returns an error if the file descriptor does not represent a bpf
// object.
func readType(fd *FD) (ObjType, error) {
s, err := os.Readlink(filepath.Join("/proc/self/fd/", fd.String()))
if err != nil {
return 0, fmt.Errorf("readlink fd %d: %w", fd.Int(), err)
}

s = strings.TrimPrefix(s, "anon_inode:")

switch s {
case "bpf-map":
return BPF_TYPE_MAP, nil
case "bpf-prog":
return BPF_TYPE_PROG, nil
case "bpf-link":
return BPF_TYPE_LINK, nil
}

return 0, fmt.Errorf("unknown type %s of fd %d", s, fd.Int())
}
11 changes: 9 additions & 2 deletions link/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ func NewFromID(id ID) (Link, error) {
return wrapRawLink(&RawLink{fd, ""})
}

// LoadPinnedLink loads a link that was persisted into a bpffs.
// LoadPinnedLink loads a Link from a pin (file) on the BPF virtual filesystem.
//
// Requires at least Linux 5.7.
func LoadPinnedLink(fileName string, opts *ebpf.LoadPinOptions) (Link, error) {
raw, err := loadPinnedRawLink(fileName, opts)
if err != nil {
Expand Down Expand Up @@ -350,14 +352,19 @@ func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
}

func loadPinnedRawLink(fileName string, opts *ebpf.LoadPinOptions) (*RawLink, error) {
fd, err := sys.ObjGet(&sys.ObjGetAttr{
fd, typ, err := sys.ObjGetTyped(&sys.ObjGetAttr{
Pathname: sys.NewStringPointer(fileName),
FileFlags: opts.Marshal(),
})
if err != nil {
return nil, fmt.Errorf("load pinned link: %w", err)
}

if typ != sys.BPF_TYPE_LINK {
_ = fd.Close()
return nil, fmt.Errorf("%s is not a Link", fileName)
}

return &RawLink{fd, fileName}, nil
}

Expand Down
27 changes: 27 additions & 0 deletions link/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,30 @@ func mustLoadProgram(tb testing.TB, typ ebpf.ProgramType, attachType ebpf.Attach

return prog
}

func TestLoadWrongPin(t *testing.T) {
cg, p := mustCgroupFixtures(t)

l, err := AttachRawLink(RawLinkOptions{
Target: int(cg.Fd()),
Program: p,
Attach: ebpf.AttachCGroupInetEgress,
})
testutils.SkipIfNotSupported(t, err)
t.Cleanup(func() { l.Close() })

tmp := testutils.TempBPFFS(t)

ppath := filepath.Join(tmp, "prog")
lpath := filepath.Join(tmp, "link")

qt.Assert(t, qt.IsNil(p.Pin(ppath)))
qt.Assert(t, qt.IsNil(l.Pin(lpath)))

_, err = LoadPinnedLink(ppath, nil)
qt.Assert(t, qt.IsNotNil(err))

ll, err := LoadPinnedLink(lpath, nil)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.IsNil(ll.Close()))
}
11 changes: 9 additions & 2 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -1560,16 +1560,23 @@ func (m *Map) unmarshalValue(value any, buf sysenc.Buffer) error {
return buf.Unmarshal(value)
}

// LoadPinnedMap loads a Map from a BPF file.
// LoadPinnedMap opens a Map from a pin (file) on the BPF virtual filesystem.
//
// Requires at least Linux 4.5.
func LoadPinnedMap(fileName string, opts *LoadPinOptions) (*Map, error) {
fd, err := sys.ObjGet(&sys.ObjGetAttr{
fd, typ, err := sys.ObjGetTyped(&sys.ObjGetAttr{
Pathname: sys.NewStringPointer(fileName),
FileFlags: opts.Marshal(),
})
if err != nil {
return nil, err
}

if typ != sys.BPF_TYPE_MAP {
_ = fd.Close()
return nil, fmt.Errorf("%s is not a Map", fileName)
}

m, err := newMapFromFD(fd)
if err == nil {
m.pinnedPath = fileName
Expand Down
32 changes: 32 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,38 @@ func TestPerfEventArrayCompatible(t *testing.T) {
qt.Assert(t, qt.IsNotNil(ms.Compatible(m)))
}

func TestLoadWrongPin(t *testing.T) {
p := mustSocketFilter(t)
m := newHash(t)
tmp := testutils.TempBPFFS(t)

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

qt.Assert(t, qt.IsNil(m.Pin(mpath)))
qt.Assert(t, qt.IsNil(p.Pin(ppath)))

t.Run("Program", func(t *testing.T) {
lp, err := LoadPinnedProgram(ppath, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.IsNil(lp.Close()))

_, err = LoadPinnedProgram(mpath, nil)
qt.Assert(t, qt.IsNotNil(err))
})

t.Run("Map", func(t *testing.T) {
lm, err := LoadPinnedMap(mpath, nil)
testutils.SkipIfNotSupported(t, err)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.IsNil(lm.Close()))

_, err = LoadPinnedMap(ppath, nil)
qt.Assert(t, qt.IsNotNil(err))
})
}

type benchValue struct {
ID uint32
Val16 uint16
Expand Down
10 changes: 8 additions & 2 deletions prog.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,18 +906,24 @@ func marshalProgram(p *Program, length int) ([]byte, error) {
return buf, nil
}

// LoadPinnedProgram loads a Program from a BPF file.
// LoadPinnedProgram loads a Program from a pin (file) on the BPF virtual
// filesystem.
//
// Requires at least Linux 4.11.
func LoadPinnedProgram(fileName string, opts *LoadPinOptions) (*Program, error) {
fd, err := sys.ObjGet(&sys.ObjGetAttr{
fd, typ, err := sys.ObjGetTyped(&sys.ObjGetAttr{
Pathname: sys.NewStringPointer(fileName),
FileFlags: opts.Marshal(),
})
if err != nil {
return nil, err
}

if typ != sys.BPF_TYPE_PROG {
_ = fd.Close()
return nil, fmt.Errorf("%s is not a Program", fileName)
}

info, err := newProgramInfoFromFd(fd)
if err != nil {
_ = fd.Close()
Expand Down

0 comments on commit d4a40af

Please sign in to comment.