sandbox/vfs: expose mountinfo line scanning
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m38s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 51s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-03-22 18:22:29 +09:00
parent 241702ae3a
commit e2fce321c1
2 changed files with 256 additions and 139 deletions
+139 -78
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"iter"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -18,13 +19,23 @@ var (
) )
type ( type (
// MountInfo represents a /proc/pid/mountinfo document. // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct {
s *bufio.Scanner
m *MountInfo
current *MountInfo
parseErr error
complete bool
}
// MountInfo represents the contents of a proc_pid_mountinfo(5) document.
MountInfo struct { MountInfo struct {
Next *MountInfo Next *MountInfo
MountInfoEntry MountInfoEntry
} }
// MountInfoEntry represents a line in /proc/pid/mountinfo. // MountInfoEntry represents a proc_pid_mountinfo(5) entry.
MountInfoEntry struct { MountInfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)). // mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"` ID int `json:"id"`
@@ -77,95 +88,145 @@ func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) {
return return
} }
// ParseMountInfo parses a mountinfo file according to proc_pid_mountinfo(5). // NewMountInfoDecoder returns a new decoder that reads from r.
func ParseMountInfo(r io.Reader) (*MountInfo, int, error) { //
var m, cur *MountInfo // The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested.
s := bufio.NewScanner(r) func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder {
return &MountInfoDecoder{s: bufio.NewScanner(r)}
var n int }
for s.Scan() {
n++ func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) {
for d.scan() {
if cur == nil { }
m = new(MountInfo) err = d.Err()
cur = m if err == nil {
} else { *v = d.m
cur.Next = new(MountInfo) }
cur = cur.Next return
}
// Entries returns an iterator over mountinfo entries.
func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
return func(yield func(*MountInfoEntry) bool) {
for cur := d.m; cur != nil; cur = cur.Next {
if !yield(&cur.MountInfoEntry) {
return
}
}
for d.scan() {
if !yield(&d.current.MountInfoEntry) {
return
}
} }
}
}
func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil {
return err
}
return d.parseErr
}
// prevent proceeding with misaligned fields due to optional fields func (d *MountInfoDecoder) scan() bool {
f := strings.Split(s.Text(), " ") if d.complete {
if len(f) < 10 { return false
return nil, -1, ErrMountInfoFields }
} if !d.s.Scan() {
d.complete = true
return false
}
m := new(MountInfo)
if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil {
d.parseErr = err
d.complete = true
return false
}
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue if d.current == nil {
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) d.m = m
d.current = d.m
} else {
d.current.Next = m
d.current = d.current.Next
}
return true
}
// (1) id func parseMountInfoLine(s string, ent *MountInfoEntry) error {
if id, err := strconv.Atoi(f[0]); err != nil { // 0 // prevent proceeding with misaligned fields due to optional fields
return nil, -1, err f := strings.Split(s, " ")
} else { if len(f) < 10 {
cur.ID = id return ErrMountInfoFields
} }
// (2) parent // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
if parent, err := strconv.Atoi(f[1]); err != nil { // 1 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
return nil, -1, err
} else {
cur.Parent = parent
}
// (3) maj:min // (1) id
if n, err := fmt.Sscanf(f[2], "%d:%d", &cur.Devno[0], &cur.Devno[1]); err != nil { if id, err := strconv.Atoi(f[0]); err != nil { // 0
return nil, -1, err return err
} else if n != 2 { } else {
// unreachable ent.ID = id
return nil, -1, ErrMountInfoDevno }
}
// (4) mountroot // (2) parent
cur.Root = Unmangle(f[3]) if parent, err := strconv.Atoi(f[1]); err != nil { // 1
if cur.Root == "" { return err
return nil, -1, ErrMountInfoEmpty } else {
} ent.Parent = parent
}
// (5) target // (3) maj:min
cur.Target = Unmangle(f[4]) if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil {
if cur.Target == "" { return err
return nil, -1, ErrMountInfoEmpty } else if n != 2 {
} // unreachable
return ErrMountInfoDevno
}
// (6) vfs options (fs-independent) // (4) mountroot
cur.VfsOptstr = Unmangle(f[5]) ent.Root = Unmangle(f[3])
if cur.VfsOptstr == "" { if ent.Root == "" {
return nil, -1, ErrMountInfoEmpty return ErrMountInfoEmpty
} }
// (7) optional fields, terminated by " - " // (5) target
i := len(f) - 4 ent.Target = Unmangle(f[4])
cur.OptFields = f[6:i] if ent.Target == "" {
return ErrMountInfoEmpty
}
// (8) optional fields end marker // (6) vfs options (fs-independent)
if f[i] != "-" { ent.VfsOptstr = Unmangle(f[5])
return nil, -1, ErrMountInfoSep if ent.VfsOptstr == "" {
} return ErrMountInfoEmpty
i++ }
// (9) FS type // (7) optional fields, terminated by " - "
cur.FsType = Unmangle(f[i]) i := len(f) - 4
if cur.FsType == "" { ent.OptFields = f[6:i]
return nil, -1, ErrMountInfoEmpty
}
i++
// (10) source -- maybe empty string // (8) optional fields end marker
cur.Source = Unmangle(f[i]) if f[i] != "-" {
i++ return ErrMountInfoSep
}
i++
// (11) fs options (fs specific) // (9) FS type
cur.FsOptstr = Unmangle(f[i]) ent.FsType = Unmangle(f[i])
if ent.FsType == "" {
return ErrMountInfoEmpty
} }
return m, n, s.Err() i++
// (10) source -- maybe empty string
ent.Source = Unmangle(f[i])
i++
// (11) fs options (fs specific)
ent.FsOptstr = Unmangle(f[i])
return nil
} }
+117 -61
View File
@@ -2,6 +2,7 @@ package vfs_test
import ( import (
"errors" "errors"
"iter"
"reflect" "reflect"
"slices" "slices"
"strconv" "strconv"
@@ -12,57 +13,62 @@ import (
"git.gensokyo.uk/security/fortify/sandbox/vfs" "git.gensokyo.uk/security/fortify/sandbox/vfs"
) )
func TestParseMountInfo(t *testing.T) { func TestMountInfo(t *testing.T) {
testCases := []struct { testCases := []mountInfoTest{
name string
sample string
wantErr error
wantError string
want []*wantMountInfo
}{
{"count", sampleMountinfoShort + ` {"count", sampleMountinfoShort + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoFields, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoFields, "", nil},
{"sep", sampleMountinfoShort + ` {"sep", sampleMountinfoShort + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw 21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoSep, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoSep, "", nil},
{"id", sampleMountinfoShort + ` {"id", sampleMountinfoShort + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, strconv.ErrSyntax, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil},
{"parent", sampleMountinfoShort + ` {"parent", sampleMountinfoShort + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, strconv.ErrSyntax, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil},
{"devno", sampleMountinfoShort + ` {"devno", sampleMountinfoShort + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, nil, "unexpected EOF", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "unexpected EOF", nil},
{"maj", sampleMountinfoShort + ` {"maj", sampleMountinfoShort + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, nil, "expected integer", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil},
{"min", sampleMountinfoShort + ` {"min", sampleMountinfoShort + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, nil, "expected integer", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil},
{"mountroot", sampleMountinfoShort + ` {"mountroot", sampleMountinfoShort + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw 21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoEmpty, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil},
{"target", sampleMountinfoShort + ` {"target", sampleMountinfoShort + `
21 20 0:53 / rw,relatime - tmpfs rw 21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoEmpty, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil},
{"vfs options", sampleMountinfoShort + ` {"vfs options", sampleMountinfoShort + `
21 20 0:53 / /mnt/test - tmpfs rw 21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoEmpty, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil},
{"FS type", sampleMountinfoShort + ` {"FS type", sampleMountinfoShort + `
21 20 0:53 / /mnt/test rw,relatime - rw 21 20 0:53 / /mnt/test rw,relatime - rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, vfs.ErrMountInfoEmpty, "", nil}, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil},
{"base", sampleMountinfoShort, nil, "", []*wantMountInfo{ {"base", sampleMountinfoShort, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
@@ -122,57 +128,107 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got, n, err := vfs.ParseMountInfo(strings.NewReader(tc.sample)) t.Run("decode", func(t *testing.T) {
if !errors.Is(err, tc.wantErr) { var got *vfs.MountInfo
if tc.wantError == "" { d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
t.Errorf("ParseMountInfo: error = %v, wantErr %v", err := d.Decode(&got)
err, tc.wantErr) tc.check(t, true,
} else if err != nil && err.Error() != tc.wantError { func(yield func(*vfs.MountInfoEntry) bool) {
t.Errorf("ParseMountInfo: error = %q, wantError %q", for cur := got; cur != nil; cur = cur.Next {
err, tc.wantError) if !yield(&cur.MountInfoEntry) {
} return
} }
}
}, func() error { return err })
t.Run("reuse", func(t *testing.T) {
tc.check(t, false,
d.Entries(), d.Err)
})
})
wantCount := len(tc.want) t.Run("iter", func(t *testing.T) {
if tc.wantErr != nil || tc.wantError != "" { d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
wantCount = -1 tc.check(t, false,
} d.Entries(), d.Err)
if n != wantCount {
t.Errorf("ParseMountInfo: got %d entries, want %d", n, wantCount)
}
i := 0 t.Run("reuse", func(t *testing.T) {
for cur := got; cur != nil; cur = cur.Next { tc.check(t, false,
if i == len(tc.want) { d.Entries(), d.Err)
t.Errorf("ParseMountInfo: got more than %d entries", len(tc.want)) })
break })
}
if !reflect.DeepEqual(&cur.MountInfoEntry, &tc.want[i].MountInfoEntry) { t.Run("yield", func(t *testing.T) {
t.Errorf("ParseMountInfo: entry %d\ngot: %#v\nwant: %#v", d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
i, cur.MountInfoEntry, tc.want[i]) v := false
} d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })
d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false })
flags, unmatched := cur.Flags() tc.check(t, false,
if flags != tc.want[i].flags { d.Entries(), d.Err)
t.Errorf("Flags(%q): %#x, want %#x",
cur.VfsOptstr, flags, tc.want[i].flags)
}
if !slices.Equal(unmatched, tc.want[i].unmatched) {
t.Errorf("Flags(%q): unmatched = %#q, want %#q",
cur.VfsOptstr, unmatched, tc.want[i].unmatched)
}
i++ t.Run("reuse", func(t *testing.T) {
} tc.check(t, false,
d.Entries(), d.Err)
if i != len(tc.want) { })
t.Errorf("ParseMountInfo: got %d entries, want %d", i, len(tc.want)) })
}
}) })
} }
} }
type mountInfoTest struct {
name string
sample string
wantErr error
wantError string
want []*wantMountInfo
}
func (tc *mountInfoTest) check(t *testing.T, checkDecode bool,
got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) {
i := 0
for cur := range got {
if i == len(tc.want) {
if !checkDecode && (tc.wantErr != nil || tc.wantError != "") {
continue
}
t.Errorf("ParseMountInfo: got more than %d entries", len(tc.want))
break
}
if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) {
t.Errorf("ParseMountInfo: entry %d\ngot: %#v\nwant: %#v",
i, cur, tc.want[i])
}
flags, unmatched := cur.Flags()
if flags != tc.want[i].flags {
t.Errorf("Flags(%q): %#x, want %#x",
cur.VfsOptstr, flags, tc.want[i].flags)
}
if !slices.Equal(unmatched, tc.want[i].unmatched) {
t.Errorf("Flags(%q): unmatched = %#q, want %#q",
cur.VfsOptstr, unmatched, tc.want[i].unmatched)
}
i++
}
if i != len(tc.want) {
t.Errorf("ParseMountInfo: got %d entries, want %d", i, len(tc.want))
}
if err := gotErr(); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("ParseMountInfo: error = %v, wantErr %v",
err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("ParseMountInfo: error = %q, wantError %q",
err, tc.wantError)
}
}
}
type wantMountInfo struct { type wantMountInfo struct {
vfs.MountInfoEntry vfs.MountInfoEntry
flags uintptr flags uintptr