Skip to content
Open
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
159 changes: 159 additions & 0 deletions internal/mountinfo/mountinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Package mountinfo parses /proc/self/mountinfo to discover filesystem
// mounts visible in the calling process's mount namespace.
package mountinfo

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"sync"

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

// Entry represents one parsed line from /proc/self/mountinfo.
type Entry struct {
// MountPoint is the absolute path where the filesystem is mounted,
// with octal escapes (\040, \011, \012, \134) decoded.
MountPoint string

// Root is the path within the source filesystem that's exposed at
// MountPoint, octal-decoded. It is "/" for a full filesystem mount and
// some other path (e.g. "/events") when a subdirectory of the source
// filesystem has been bind-mounted onto MountPoint. Callers that want
// to use MountPoint as the root of a filesystem should reject entries
// where Root != "/".
Root string

// FSType is the filesystem type, e.g. "bpf", "tracefs", "debugfs".
FSType string
}

// Read parses /proc/self/mountinfo and returns one Entry per mount line.
// The result is cached for the lifetime of the process; mounts that appear
// or disappear after the first call are not reflected.
//
// On non-Linux, returns an error wrapping [internal.ErrNotSupportedOnOS].
func Read() ([]Entry, error) {
return readOnce()
}

// FindByFSType returns the mount points for filesystems of the given type,
// in order of appearance, with duplicates removed. A single mount point
// may legitimately appear multiple times (for example, a bpffs mounted
// twice with different delegation options) — callers typically only care
// about the path, so duplicates are filtered.
func FindByFSType(fstype string) ([]string, error) {
entries, err := Read()
if err != nil {
return nil, err
}
return filterByFSType(entries, fstype), nil
}

func filterByFSType(entries []Entry, fstype string) []string {
var mounts []string
for _, e := range entries {
if e.FSType != fstype {
continue
}
// Number of matching mounts is expected to be very low; linear
// search is faster and more cache-friendly than a map at this
// scale, and avoids the allocations a map would incur.
if !slices.Contains(mounts, e.MountPoint) {
mounts = append(mounts, e.MountPoint)
}
}
return mounts
}

// ParseEntries parses mountinfo data read from r. Exposed for tests; most
// callers want [Read] or [FindByFSType].
//
// Uses [bufio.Reader] rather than [bufio.Scanner] because mountinfo lines
// can exceed Scanner's default 64 KiB token limit — overlay filesystems
// in containers commonly produce single mount entries with many KB of
// lowerdir options.
func ParseEntries(r io.Reader) ([]Entry, error) {
var entries []Entry
// Format of /proc/self/mountinfo:
//
// {id} {parent id} {major:minor} {root} {mount point} {mount options} [optional fields...] - {filesystem type} {source} {superblock options}
br := bufio.NewReader(r)
for {
line, err := br.ReadString('\n')
if line != "" {
line = strings.TrimRight(line, "\n")
if line != "" {
entry, perr := parseLine(line)
if perr != nil {
return nil, perr
}
entries = append(entries, entry)
}
}
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, fmt.Errorf("read mountinfo: %w", err)
}
}

return entries, nil
}

func parseLine(line string) (Entry, error) {
firstHalfStr, secondHalfStr, ok := strings.Cut(line, " - ")
if !ok {
return Entry{}, fmt.Errorf("invalid mountinfo line, missing dash: %q", line)
}

secondHalf := strings.Fields(strings.TrimSpace(secondHalfStr))
if len(secondHalf) == 0 {
return Entry{}, fmt.Errorf("invalid mountinfo line, too few fields after dash: %q", line)
}

firstHalf := strings.Fields(strings.TrimSpace(firstHalfStr))
if len(firstHalf) < 6 {
return Entry{}, fmt.Errorf("invalid mountinfo line, too few fields: %q", line)
}

return Entry{
Root: unescape(firstHalf[3]),
MountPoint: unescape(firstHalf[4]),
FSType: secondHalf[0],
}, nil
}

// show_mountinfo in the kernel has the escape set of ' \t\n\\'. Instead of
// a full octal unescaper, only replace these specific characters.
var unescaper = strings.NewReplacer(
`\040`, " ",
`\011`, "\t",
`\012`, "\n",
`\134`, `\`,
)

func unescape(s string) string {
return unescaper.Replace(s)
}

var readOnce = sync.OnceValues(func() ([]Entry, error) {
if !platform.IsLinux {
return nil, fmt.Errorf("mountinfo: %w", internal.ErrNotSupportedOnOS)
}

f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return nil, fmt.Errorf("open mountinfo: %w", err)
}
defer f.Close()

return ParseEntries(f)
})
132 changes: 132 additions & 0 deletions internal/mountinfo/mountinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package mountinfo

import (
"strings"
"testing"

"github.com/go-quicktest/qt"
)

func TestParseEntriesPreservesRoot(t *testing.T) {
// Subdirectory bind mounts retain their original fstype but expose a non-/
// root field, so callers can distinguish them from full filesystem mounts.
const mountinfo = `
39 29 0:12 / /sys/kernel/tracing rw - tracefs tracefs rw
40 29 0:12 /events /weird/tracing-events rw - tracefs tracefs rw
`

entries, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.HasLen(entries, 2))
qt.Assert(t, qt.Equals(entries[0].Root, "/"))
qt.Assert(t, qt.Equals(entries[1].Root, "/events"))
}

func TestParseEntriesLongLine(t *testing.T) {
// Overlay lowerdir lists in containers commonly exceed bufio.Scanner's
// default 64 KiB token limit. Build a synthetic line just over that
// threshold and assert that parsing succeeds and surfaces the entry.
var lowers []string
for i := 0; i < 1000; i++ {
lowers = append(lowers, "/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/12345/fs")
}
long := "8 23 0:23 / / rw,relatime - overlay overlay rw,lowerdir=" + strings.Join(lowers, ":")
if len(long) <= 64*1024 {
t.Fatalf("test fixture not long enough: %d bytes", len(long))
}
mountinfo := long + "\n39 29 0:12 / /sys/kernel/tracing rw - tracefs tracefs rw\n"

entries, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.HasLen(entries, 2))
qt.Assert(t, qt.Equals(entries[0].FSType, "overlay"))
qt.Assert(t, qt.Equals(entries[1].MountPoint, "/sys/kernel/tracing"))
qt.Assert(t, qt.Equals(entries[1].FSType, "tracefs"))
}

func TestParseEntries(t *testing.T) {
const mountinfo = `
8 23 0:23 / / rw,relatime - overlay overlay rw,lowerdir=/overlay:/host,upperdir=/upper,workdir=/work,uuid=on
29 28 0:27 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sys rw
30 28 0:28 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
35 29 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700
36 29 0:30 / /sys/fs/foo\040bar\040baz rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700
37 29 0:30 / /sys/fs/功能\011\012\134bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700
38 29 0:8 / /sys/kernel/debug rw,nosuid,nodev,noexec,relatime - debugfs debugfs rw
39 29 0:12 / /sys/kernel/tracing rw,nosuid,nodev,noexec,relatime - tracefs tracefs rw
`

entries, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.HasLen(entries, 8))

qt.Assert(t, qt.Equals(entries[0].MountPoint, "/"))
qt.Assert(t, qt.Equals(entries[0].FSType, "overlay"))

qt.Assert(t, qt.Equals(entries[3].MountPoint, "/sys/fs/bpf"))
qt.Assert(t, qt.Equals(entries[3].FSType, "bpf"))

// Octal escapes for space, tab, newline, backslash.
qt.Assert(t, qt.Equals(entries[4].MountPoint, "/sys/fs/foo bar baz"))
qt.Assert(t, qt.Equals(entries[5].MountPoint, "/sys/fs/功能\t\n\\bpf"))

qt.Assert(t, qt.Equals(entries[6].MountPoint, "/sys/kernel/debug"))
qt.Assert(t, qt.Equals(entries[6].FSType, "debugfs"))

qt.Assert(t, qt.Equals(entries[7].MountPoint, "/sys/kernel/tracing"))
qt.Assert(t, qt.Equals(entries[7].FSType, "tracefs"))
}

func TestParseEntriesInvalid(t *testing.T) {
t.Run("missing dash separator", func(t *testing.T) {
const mountinfo = `48 46 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime`
_, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNotNil(err))
})

t.Run("too few fields before dash", func(t *testing.T) {
const mountinfo = `48 46 0:30 / /sys/fs/bpf - bpf bpf rw`
_, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNotNil(err))
})

t.Run("missing fstype after dash", func(t *testing.T) {
const mountinfo = `48 46 0:30 / /sys/fs/bpf rw,nosuid - `
_, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNotNil(err))
})
}

func TestFindByFSType(t *testing.T) {
const mountinfo = `
35 29 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700
36 29 0:30 / /sys/fs/bpf rw,relatime - bpf none rw,delegate_cmds=prog_load
37 29 0:30 / /run/tw/bpf rw,relatime - bpf none rw,delegate_cmds=prog_load
38 29 0:8 / /sys/kernel/debug rw,nosuid,nodev,noexec,relatime - debugfs debugfs rw
39 29 0:12 / /sys/kernel/tracing rw,nosuid,nodev,noexec,relatime - tracefs tracefs rw
40 29 0:13 / /custom/tracing rw - tracefs tracefs rw
`

entries, err := ParseEntries(strings.NewReader(mountinfo))
qt.Assert(t, qt.IsNil(err))

t.Run("filters and dedupes by mount point", func(t *testing.T) {
got := filterByFSType(entries, "bpf")
qt.Assert(t, qt.DeepEquals(got, []string{"/sys/fs/bpf", "/run/tw/bpf"}))
})

t.Run("returns all matching mount points in order", func(t *testing.T) {
got := filterByFSType(entries, "tracefs")
qt.Assert(t, qt.DeepEquals(got, []string{"/sys/kernel/tracing", "/custom/tracing"}))
})

t.Run("single match", func(t *testing.T) {
got := filterByFSType(entries, "debugfs")
qt.Assert(t, qt.DeepEquals(got, []string{"/sys/kernel/debug"}))
})

t.Run("no match returns empty", func(t *testing.T) {
got := filterByFSType(entries, "nfs")
qt.Assert(t, qt.HasLen(got, 0))
})
}
79 changes: 2 additions & 77 deletions internal/sys/token.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package sys

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"sync"
"unsafe"

"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/mountinfo"
"github.com/cilium/ebpf/internal/platform"
"github.com/cilium/ebpf/internal/testutils/testmain"
"github.com/cilium/ebpf/internal/unix"
Expand Down Expand Up @@ -136,7 +132,7 @@ func findToken() (*Token, error) {
return nil, fmt.Errorf("bpf token: %w", internal.ErrNotSupportedOnOS)
}

mounts, err := readBPFFSMounts()
mounts, err := mountinfo.FindByFSType("bpf")
if err != nil {
return nil, fmt.Errorf("get bpffs mounts: %w", err)
}
Expand Down Expand Up @@ -192,77 +188,6 @@ func newToken(mount string) (*Token, error) {
return t, nil
}

var readBPFFSMounts = sync.OnceValues(func() ([]string, error) {
mountinfo, err := os.Open("/proc/self/mountinfo")
if err != nil {
return nil, err
}
defer mountinfo.Close()

return parseBPFFSMounts(mountinfo)
})

func parseBPFFSMounts(r io.Reader) ([]string, error) {
var mounts []string
// Format of /proc/self/mountinfo:
//
// {id} {parent id} {major:minor} {root} {mount point} {mount options} [optional fields...] - {filesystem type} {source} {superblock options}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}

firstHalfStr, secondHalfStr, ok := strings.Cut(line, " - ")
if !ok {
return nil, fmt.Errorf("invalid mountinfo line, missing dash: %q", line)
}

secondHalf := strings.Fields(strings.TrimSpace(secondHalfStr))
if len(secondHalf) == 0 {
return nil, fmt.Errorf("invalid mountinfo line, too few fields after dash: %q", line)
}

fstype := secondHalf[0]
if fstype != "bpf" {
continue
}

firstHalf := strings.Fields(strings.TrimSpace(firstHalfStr))
if len(firstHalf) < 6 {
return nil, fmt.Errorf("invalid mountinfo line, too few fields: %q", line)
}

mountPoint := unescape(firstHalf[4])
if !slices.Contains(mounts, mountPoint) {
// Number of mounts is expected to be very low. Map lookups are more
// expensive and cache-unfriendly at this scale, and converting to a map
// would incur extra allocations and copies.
mounts = append(mounts, mountPoint)
}
}

if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan mountinfo: %w", err)
}

return mounts, nil
}

// show_mountinfo in the kernel has the escape set of ' \t\n\\'. Instead of a
// full octal unescaper, only replace these specific characters.
var unescaper = strings.NewReplacer(
`\040`, " ",
`\011`, "\t",
`\012`, "\n",
`\134`, `\`,
)

func unescape(s string) string {
return unescaper.Replace(s)
}

// tokenAttr sets the appropriate token fields in the BPF syscall attribute
// struct for the given command, if a token is available.
func tokenAttr(cmd Cmd, attr unsafe.Pointer) (*Token, error) {
Expand Down
Loading