From 02c30d5911d0bf45315439d17c523594003ccebb Mon Sep 17 00:00:00 2001 From: Yoav Alon Date: Fri, 8 May 2026 14:51:22 +0300 Subject: [PATCH] tracefs: discover mount via /proc/self/mountinfo Lift the BPFFS mountinfo parser out of internal/sys into a new internal/mountinfo package, parameterized by filesystem type, and reuse it in internal/tracefs. Tracefs auto-detection now consults /proc/self/mountinfo first: any tracefs mount is accepted regardless of its path, and a debugfs mount with an existing tracing/ subdirectory is used as a fallback. The hardcoded /sys/kernel/{tracing,debug/tracing} probe via statfs remains as a last-resort fallback for environments where mountinfo is unavailable, and to keep behavior identical on kernels that predate tracefs being lifted out of debugfs (Linux 4.1). This removes the need for callers in containerized environments to bind-mount tracefs at the kernel-canonical paths just to make cilium/ebpf find it. internal/sys/token.go shrinks accordingly: parseBPFFSMounts, readBPFFSMounts, and the unescape helper now live in the new package and are tested there. Closes #2000 Signed-off-by: Yoav Alon --- internal/mountinfo/mountinfo.go | 159 +++++++++++++++++++++++++++ internal/mountinfo/mountinfo_test.go | 132 ++++++++++++++++++++++ internal/sys/token.go | 79 +------------ internal/sys/token_test.go | 63 ----------- internal/tracefs/kprobe.go | 51 ++++++++- internal/tracefs/perf_event_test.go | 92 ++++++++++++++++ 6 files changed, 432 insertions(+), 144 deletions(-) create mode 100644 internal/mountinfo/mountinfo.go create mode 100644 internal/mountinfo/mountinfo_test.go delete mode 100644 internal/sys/token_test.go diff --git a/internal/mountinfo/mountinfo.go b/internal/mountinfo/mountinfo.go new file mode 100644 index 000000000..99d842115 --- /dev/null +++ b/internal/mountinfo/mountinfo.go @@ -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) +}) diff --git a/internal/mountinfo/mountinfo_test.go b/internal/mountinfo/mountinfo_test.go new file mode 100644 index 000000000..869d86c93 --- /dev/null +++ b/internal/mountinfo/mountinfo_test.go @@ -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)) + }) +} diff --git a/internal/sys/token.go b/internal/sys/token.go index 8e8b77c81..9a5df3922 100644 --- a/internal/sys/token.go +++ b/internal/sys/token.go @@ -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" @@ -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) } @@ -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) { diff --git a/internal/sys/token_test.go b/internal/sys/token_test.go deleted file mode 100644 index 2acc5db40..000000000 --- a/internal/sys/token_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package sys - -import ( - "strings" - "testing" - - "github.com/go-quicktest/qt" -) - -func TestParseMounts(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 -31 28 0:6 / /dev rw,nosuid - devtmpfs devtmpfs rw,size=496012k,nr_inodes=124003,mode=755 -32 31 0:29 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -33 29 0:7 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime - securityfs securityfs rw -34 29 0:7 / /sys/dash-dir bogus,options - bogusfs bogusfs ro,bogus=true-opt -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 -` - - mounts, err := parseBPFFSMounts(strings.NewReader(mountinfo)) - qt.Assert(t, qt.IsNil(err)) - qt.Assert(t, qt.HasLen(mounts, 3)) - qt.Assert(t, qt.Equals(mounts[0], "/sys/fs/bpf")) - qt.Assert(t, qt.Equals(mounts[1], "/sys/fs/foo bar baz")) - qt.Assert(t, qt.Equals(mounts[2], "/sys/fs/功能\t\n\\bpf")) -} - -func TestParseMountsSamePath(t *testing.T) { - const mountinfo = ` -48 46 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700 -58 48 0:35 / /sys/fs/bpf rw,relatime - bpf none rw,delegate_cmds=prog_load -` - - mounts, err := parseBPFFSMounts(strings.NewReader(mountinfo)) - qt.Assert(t, qt.IsNil(err)) - qt.Assert(t, qt.HasLen(mounts, 1)) - qt.Assert(t, qt.Equals(mounts[0], "/sys/fs/bpf")) -} - -func TestParseMountsMultiple(t *testing.T) { - const mountinfo = ` -48 46 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700 -58 48 0:35 / /run/tw/bpf rw,relatime - bpf none rw,delegate_cmds=prog_load -` - - mounts, err := parseBPFFSMounts(strings.NewReader(mountinfo)) - qt.Assert(t, qt.IsNil(err)) - qt.Assert(t, qt.HasLen(mounts, 2)) - qt.Assert(t, qt.Equals(mounts[0], "/sys/fs/bpf")) - qt.Assert(t, qt.Equals(mounts[1], "/run/tw/bpf")) -} - -func TestParseMountsInvalid(t *testing.T) { - const mountinfo = `48 46 0:30 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime` - _, err := parseBPFFSMounts(strings.NewReader(mountinfo)) - qt.Assert(t, qt.IsNotNil(err)) -} diff --git a/internal/tracefs/kprobe.go b/internal/tracefs/kprobe.go index d0b5be66c..f8dfc5d01 100644 --- a/internal/tracefs/kprobe.go +++ b/internal/tracefs/kprobe.go @@ -13,6 +13,7 @@ import ( "github.com/cilium/ebpf/internal" "github.com/cilium/ebpf/internal/linux" + "github.com/cilium/ebpf/internal/mountinfo" "github.com/cilium/ebpf/internal/platform" "github.com/cilium/ebpf/internal/unix" ) @@ -109,15 +110,31 @@ func sanitizeTracefsPath(path ...string) (string, error) { return p, nil } -// getTracefsPath will return a correct path to the tracefs mount point. -// Since kernel 4.1 tracefs should be mounted by default at /sys/kernel/tracing, -// but may be also be available at /sys/kernel/debug/tracing if debugfs is mounted. -// The available tracefs paths will depends on distribution choices. +// getTracefsPath returns a correct path to the tracefs mount point. +// +// The discovery order is: +// +// 1. Any tracefs mount listed in /proc/self/mountinfo (kernel 4.1+). +// This works regardless of where the mount sits in the filesystem, +// so containers that bind-mount tracefs at a non-canonical path are +// supported automatically. +// 2. A debugfs mount with a tracing/ subdirectory, for older systems +// where tracefs has not been lifted out of debugfs. +// 3. As a final fallback, probe the canonical kernel paths +// (/sys/kernel/tracing, /sys/kernel/debug/tracing) directly with +// statfs. This catches edge cases where /proc/self/mountinfo is +// unavailable or doesn't report the mount. var getTracefsPath = sync.OnceValues(func() (string, error) { if !platform.IsLinux { return "", fmt.Errorf("tracefs: %w", internal.ErrNotSupportedOnOS) } + if entries, err := mountinfo.Read(); err == nil { + if path, err := findTracefsInEntries(entries); err == nil && path != "" { + return path, nil + } + } + for _, p := range []struct { path string fsType int64 @@ -135,6 +152,32 @@ var getTracefsPath = sync.OnceValues(func() (string, error) { return "", errors.New("neither debugfs nor tracefs are mounted") }) +// findTracefsInEntries scans a list of mount entries for tracefs, then for +// debugfs with an existing tracing/ subdirectory. Subdirectory bind mounts +// (entries whose Root field is not "/") are skipped, since their mount +// point doesn't expose the tracefs root files (kprobe_events, +// uprobe_events, events/) that callers need. +// +// Returns the empty string (and a nil error) when no usable mount is +// found, leaving the caller to fall back to canonical-path probing. +func findTracefsInEntries(entries []mountinfo.Entry) (string, error) { + for _, e := range entries { + if e.FSType == "tracefs" && e.Root == "/" { + return e.MountPoint, nil + } + } + for _, e := range entries { + if e.FSType != "debugfs" || e.Root != "/" { + continue + } + tracing := filepath.Join(e.MountPoint, "tracing") + if info, err := os.Stat(tracing); err == nil && info.IsDir() { + return tracing, nil + } + } + return "", nil +} + // sanitizeIdentifier replaces every invalid character for the tracefs api with an underscore. // // It is equivalent to calling regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString("_"). diff --git a/internal/tracefs/perf_event_test.go b/internal/tracefs/perf_event_test.go index b59478c7c..176ff6d24 100644 --- a/internal/tracefs/perf_event_test.go +++ b/internal/tracefs/perf_event_test.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "os" + "path/filepath" "testing" "github.com/go-quicktest/qt" + "github.com/cilium/ebpf/internal/mountinfo" "github.com/cilium/ebpf/internal/testutils" ) @@ -91,3 +93,93 @@ func TestGetTracefsPath(t *testing.T) { _, err = os.Stat(path) qt.Assert(t, qt.IsNil(err)) } + +func TestFindTracefsInEntries(t *testing.T) { + tmpDebugWithTracing := t.TempDir() + qt.Assert(t, qt.IsNil(os.Mkdir(filepath.Join(tmpDebugWithTracing, "tracing"), 0o755))) + tmpDebugBare := t.TempDir() + + t.Run("tracefs entry returns its mount point", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: "/host/sys/kernel/tracing", Root: "/", FSType: "tracefs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "/host/sys/kernel/tracing")) + }) + + t.Run("first tracefs entry wins when multiple", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: "/sys/kernel/tracing", Root: "/", FSType: "tracefs"}, + {MountPoint: "/host/sys/kernel/tracing", Root: "/", FSType: "tracefs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "/sys/kernel/tracing")) + }) + + t.Run("tracefs preferred over debugfs", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: tmpDebugWithTracing, Root: "/", FSType: "debugfs"}, + {MountPoint: "/sys/kernel/tracing", Root: "/", FSType: "tracefs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "/sys/kernel/tracing")) + }) + + t.Run("debugfs entry with tracing subdir returns the subdir", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: tmpDebugWithTracing, Root: "/", FSType: "debugfs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, filepath.Join(tmpDebugWithTracing, "tracing"))) + }) + + t.Run("debugfs entry without tracing subdir falls through", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: tmpDebugBare, Root: "/", FSType: "debugfs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "")) + }) + + t.Run("no tracefs and no usable debugfs returns empty", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: "/", Root: "/", FSType: "overlay"}, + {MountPoint: "/proc", Root: "/", FSType: "proc"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "")) + }) + + t.Run("nil entries returns empty", func(t *testing.T) { + got, err := findTracefsInEntries(nil) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "")) + }) + + t.Run("subdirectory bind mount is skipped in favor of real tracefs", func(t *testing.T) { + // /weird/tracing-events is a bind mount of tracefs's events/ subdir; + // it should not be picked even though it appears first. + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: "/weird/tracing-events", Root: "/events", FSType: "tracefs"}, + {MountPoint: "/sys/kernel/tracing", Root: "/", FSType: "tracefs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "/sys/kernel/tracing")) + }) + + t.Run("only subdirectory tracefs mounts means no usable tracefs", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: "/weird/tracing-events", Root: "/events", FSType: "tracefs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "")) + }) + + t.Run("subdirectory debugfs bind mount is skipped", func(t *testing.T) { + got, err := findTracefsInEntries([]mountinfo.Entry{ + {MountPoint: tmpDebugWithTracing, Root: "/tracing", FSType: "debugfs"}, + }) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(got, "")) + }) +}