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, "")) + }) +}