sandbox/vfs: expose mountinfo line scanning
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m26s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 4m2s
Test / Flake checks (push) Successful in 49s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-03-22 18:22:29 +09:00
parent 241702ae3a
commit 66908ddc05
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
2 changed files with 246 additions and 135 deletions

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
} }
// prevent proceeding with misaligned fields due to optional fields // Entries returns an iterator over mountinfo entries.
f := strings.Split(s.Text(), " ") func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
if len(f) < 10 { return func(yield func(*MountInfoEntry) bool) {
return nil, -1, ErrMountInfoFields for cur := d.m; cur != nil; cur = cur.Next {
} if !yield(&cur.MountInfoEntry) {
return
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue }
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) }
for d.scan() {
// (1) id if !yield(&d.current.MountInfoEntry) {
if id, err := strconv.Atoi(f[0]); err != nil { // 0 return
return nil, -1, err }
} else { }
cur.ID = id }
} }
// (2) parent func (d *MountInfoDecoder) Err() error {
if parent, err := strconv.Atoi(f[1]); err != nil { // 1 if err := d.s.Err(); err != nil {
return nil, -1, err return err
} else { }
cur.Parent = parent return d.parseErr
} }
// (3) maj:min func (d *MountInfoDecoder) scan() bool {
if n, err := fmt.Sscanf(f[2], "%d:%d", &cur.Devno[0], &cur.Devno[1]); err != nil { if d.complete {
return nil, -1, err return false
} else if n != 2 { }
// unreachable if !d.s.Scan() {
return nil, -1, ErrMountInfoDevno d.complete = true
} return false
}
// (4) mountroot
cur.Root = Unmangle(f[3]) m := new(MountInfo)
if cur.Root == "" { if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil {
return nil, -1, ErrMountInfoEmpty d.parseErr = err
} d.complete = true
return false
// (5) target }
cur.Target = Unmangle(f[4])
if cur.Target == "" { if d.current == nil {
return nil, -1, ErrMountInfoEmpty d.m = m
} d.current = d.m
} else {
// (6) vfs options (fs-independent) d.current.Next = m
cur.VfsOptstr = Unmangle(f[5]) d.current = d.current.Next
if cur.VfsOptstr == "" { }
return nil, -1, ErrMountInfoEmpty return true
} }
// (7) optional fields, terminated by " - " func parseMountInfoLine(s string, ent *MountInfoEntry) error {
i := len(f) - 4 // prevent proceeding with misaligned fields due to optional fields
cur.OptFields = f[6:i] f := strings.Split(s, " ")
if len(f) < 10 {
// (8) optional fields end marker return ErrMountInfoFields
if f[i] != "-" { }
return nil, -1, ErrMountInfoSep
} // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
i++ // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// (9) FS type // (1) id
cur.FsType = Unmangle(f[i]) if id, err := strconv.Atoi(f[0]); err != nil { // 0
if cur.FsType == "" { return err
return nil, -1, ErrMountInfoEmpty } else {
} ent.ID = id
i++ }
// (10) source -- maybe empty string // (2) parent
cur.Source = Unmangle(f[i]) if parent, err := strconv.Atoi(f[1]); err != nil { // 1
i++ return err
} else {
// (11) fs options (fs specific) ent.Parent = parent
cur.FsOptstr = Unmangle(f[i]) }
}
return m, n, s.Err() // (3) maj:min
if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil {
return err
} else if n != 2 {
// unreachable
return ErrMountInfoDevno
}
// (4) mountroot
ent.Root = Unmangle(f[3])
if ent.Root == "" {
return ErrMountInfoEmpty
}
// (5) target
ent.Target = Unmangle(f[4])
if ent.Target == "" {
return ErrMountInfoEmpty
}
// (6) vfs options (fs-independent)
ent.VfsOptstr = Unmangle(f[5])
if ent.VfsOptstr == "" {
return ErrMountInfoEmpty
}
// (7) optional fields, terminated by " - "
i := len(f) - 4
ent.OptFields = f[6:i]
// (8) optional fields end marker
if f[i] != "-" {
return ErrMountInfoSep
}
i++
// (9) FS type
ent.FsType = Unmangle(f[i])
if ent.FsType == "" {
return ErrMountInfoEmpty
}
i++
// (10) source -- maybe empty string
ent.Source = Unmangle(f[i])
i++
// (11) fs options (fs specific)
ent.FsOptstr = Unmangle(f[i])
return nil
} }

View File

@ -2,6 +2,7 @@ package vfs_test
import ( import (
"errors" "errors"
"iter"
"reflect" "reflect"
"slices" "slices"
"strconv" "strconv"
@ -122,57 +123,106 @@ 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) checkMountInfo(t, true,
} else if err != nil && err.Error() != tc.wantError { tc.want, tc.wantErr, tc.wantError,
t.Errorf("ParseMountInfo: error = %q, wantError %q", func(yield func(*vfs.MountInfoEntry) bool) {
err, tc.wantError) for cur := got; cur != nil; cur = cur.Next {
} if !yield(&cur.MountInfoEntry) {
} return
}
}
}, func() error { return err })
t.Run("reuse", func(t *testing.T) {
checkMountInfo(t, false,
tc.want, tc.wantErr, tc.wantError,
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 checkMountInfo(t, false,
} tc.want, tc.wantErr, tc.wantError,
if n != wantCount { d.Entries(), d.Err)
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 { checkMountInfo(t, false,
if i == len(tc.want) { tc.want, tc.wantErr, tc.wantError,
t.Errorf("ParseMountInfo: got more than %d entries", len(tc.want)) d.Entries(), d.Err)
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() checkMountInfo(t, false,
if flags != tc.want[i].flags { tc.want, tc.wantErr, tc.wantError,
t.Errorf("Flags(%q): %#x, want %#x", d.Entries(), d.Err)
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) {
} checkMountInfo(t, false,
tc.want, tc.wantErr, tc.wantError,
if i != len(tc.want) { d.Entries(), d.Err)
t.Errorf("ParseMountInfo: got %d entries, want %d", i, len(tc.want)) })
} })
}) })
} }
} }
func checkMountInfo(t *testing.T, checkDecode bool,
want []*wantMountInfo, wantErr error, wantError string,
got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) {
i := 0
for cur := range got {
if i == len(want) {
if !checkDecode && (wantErr != nil || wantError != "") {
continue
}
t.Errorf("ParseMountInfo: got more than %d entries", len(want))
break
}
if !reflect.DeepEqual(cur, &want[i].MountInfoEntry) {
t.Errorf("ParseMountInfo: entry %d\ngot: %#v\nwant: %#v",
i, cur, want[i])
}
flags, unmatched := cur.Flags()
if flags != want[i].flags {
t.Errorf("Flags(%q): %#x, want %#x",
cur.VfsOptstr, flags, want[i].flags)
}
if !slices.Equal(unmatched, want[i].unmatched) {
t.Errorf("Flags(%q): unmatched = %#q, want %#q",
cur.VfsOptstr, unmatched, want[i].unmatched)
}
i++
}
if i != len(want) {
t.Errorf("ParseMountInfo: got %d entries, want %d", i, len(want))
}
if err := gotErr(); !errors.Is(err, wantErr) {
if wantError == "" {
t.Errorf("ParseMountInfo: error = %v, wantErr %v",
err, wantErr)
} else if err != nil && err.Error() != wantError {
t.Errorf("ParseMountInfo: error = %q, wantError %q",
err, wantError)
}
}
}
type wantMountInfo struct { type wantMountInfo struct {
vfs.MountInfoEntry vfs.MountInfoEntry
flags uintptr flags uintptr