From a11237b15848cc99d117ca6eb832eed0c92c64ec Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 24 Mar 2025 13:21:55 +0900 Subject: sandbox/vfs: add doc comments Signed-off-by: Ophestra --- sandbox/container_test.go | 2 +- sandbox/vfs/mount.go | 107 --------------------------------------------- sandbox/vfs/mount_test.go | 93 --------------------------------------- sandbox/vfs/mountinfo.go | 18 ++++---- sandbox/vfs/unfold.go | 107 +++++++++++++++++++++++++++++++++++++++++++++ sandbox/vfs/unfold_test.go | 93 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 209 deletions(-) delete mode 100644 sandbox/vfs/mount.go delete mode 100644 sandbox/vfs/mount_test.go create mode 100644 sandbox/vfs/unfold.go create mode 100644 sandbox/vfs/unfold_test.go (limited to 'sandbox') diff --git a/sandbox/container_test.go b/sandbox/container_test.go index 5ed9fee4..f6e6562d 100644 --- a/sandbox/container_test.go +++ b/sandbox/container_test.go @@ -231,7 +231,7 @@ func TestHelperCheckContainer(t *testing.T) { mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime") mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") - if !cur.EqualWithIgnore(mnt[i]) { + if !cur.EqualWithIgnore(mnt[i], "\x00") { t.Errorf("[FAIL] %s", cur) } else { t.Logf("[ OK ] %s", cur) diff --git a/sandbox/vfs/mount.go b/sandbox/vfs/mount.go deleted file mode 100644 index ca0e37b7..00000000 --- a/sandbox/vfs/mount.go +++ /dev/null @@ -1,107 +0,0 @@ -package vfs - -import ( - "iter" - "path" - "strings" - "syscall" -) - -// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. -type MountInfoNode struct { - *MountInfoEntry - FirstChild *MountInfoNode `json:"first_child"` - NextSibling *MountInfoNode `json:"next_sibling"` - - Clean string `json:"clean"` - Covered bool `json:"covered"` -} - -// Collective returns an iterator over visible mountinfo nodes. -func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] { - return func(yield func(*MountInfoNode) bool) { n.visit(yield) } -} - -func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool { - if !n.Covered && !yield(n) { - return false - } - for cur := n.FirstChild; cur != nil; cur = cur.NextSibling { - if !cur.visit(yield) { - return false - } - } - return true -} - -// Unfold unfolds the mount hierarchy and resolves covered paths. -func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) { - targetClean := path.Clean(target) - - var mountinfoSize int - for range d.Entries() { - mountinfoSize++ - } - if err := d.Err(); err != nil { - return nil, err - } - - mountinfo := make([]*MountInfoNode, mountinfoSize) - // mount ID to index lookup - idIndex := make(map[int]int, mountinfoSize) - // final entry to match target - targetIndex := -1 - { - i := 0 - for ent := range d.Entries() { - mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent} - idIndex[ent.ID] = i - if mountinfo[i].Clean == targetClean { - targetIndex = i - } - - i++ - } - } - - if targetIndex == -1 { - return nil, syscall.ESTALE - } - - for _, cur := range mountinfo { - var parent *MountInfoNode - if p, ok := idIndex[cur.Parent]; !ok { - continue - } else { - parent = mountinfo[p] - } - - if !strings.HasPrefix(cur.Clean, targetClean) { - continue - } - if parent.Clean == cur.Clean { - parent.Covered = true - } - - covered := false - nsp := &parent.FirstChild - for s := parent.FirstChild; s != nil; s = s.NextSibling { - if strings.HasPrefix(cur.Clean, s.Clean) { - covered = true - break - } - - if strings.HasPrefix(s.Clean, cur.Clean) { - *nsp = s.NextSibling - } else { - nsp = &s.NextSibling - } - } - if covered { - continue - } - *nsp = cur - } - - return mountinfo[targetIndex], nil -} diff --git a/sandbox/vfs/mount_test.go b/sandbox/vfs/mount_test.go deleted file mode 100644 index 4a262567..00000000 --- a/sandbox/vfs/mount_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package vfs_test - -import ( - "errors" - "reflect" - "slices" - "strings" - "syscall" - "testing" - - "git.gensokyo.uk/security/fortify/sandbox/vfs" -) - -func TestUnfold(t *testing.T) { - testCases := []struct { - name string - sample string - target string - wantErr error - - want *vfs.MountInfoNode - wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode - wantCollectN []string - }{ - { - "no match", - sampleMountinfoBase, - "/mnt", - syscall.ESTALE, nil, nil, nil, - }, - { - "cover", - `33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw -551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw -595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw -611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw -625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw -641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -`, "/mnt", nil, - mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false, - mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false, - mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true, - mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true, - mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false, - nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { - return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild} - }, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) - got, err := d.Unfold(tc.target) - - if !errors.Is(err, tc.wantErr) { - t.Errorf("Unfold: error = %v, wantErr %v", - err, tc.wantErr) - } - - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("Unfold:\ngot %s\nwant %s", - mustMarshal(got), mustMarshal(tc.want)) - } - - if err == nil && tc.wantCollectF != nil { - t.Run("collective", func(t *testing.T) { - wantCollect := tc.wantCollectF(got) - gotCollect := slices.Collect(got.Collective()) - if !reflect.DeepEqual(gotCollect, wantCollect) { - t.Errorf("Collective: \ngot %#v\nwant %#v", - gotCollect, wantCollect) - } - t.Run("target", func(t *testing.T) { - gotCollectN := slices.Collect[string](func(yield func(v string) bool) { - for _, cur := range gotCollect { - if !yield(cur.Clean) { - return - } - } - }) - if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) { - t.Errorf("Collective: got %q, want %q", - gotCollectN, tc.wantCollectN) - } - }) - }) - } - }) - } -} diff --git a/sandbox/vfs/mountinfo.go b/sandbox/vfs/mountinfo.go index e0c9d411..bcb3063f 100644 --- a/sandbox/vfs/mountinfo.go +++ b/sandbox/vfs/mountinfo.go @@ -1,3 +1,4 @@ +// Package vfs provides bindings and iterators over proc_pid_mountinfo(5). package vfs import ( @@ -68,6 +69,7 @@ type ( DevT [2]int ) +// Flags interprets VfsOptstr and returns the resulting flags and unmatched options. func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) { for _, s := range strings.Split(e.VfsOptstr, ",") { switch s { @@ -238,17 +240,17 @@ func parseMountInfoLine(s string, ent *MountInfoEntry) error { return nil } -func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry) bool { +func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool { return (e.ID == want.ID || want.ID == -1) && (e.Parent == want.Parent || want.Parent == -1) && (e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) && - (e.Root == want.Root || want.Root == "\x00") && - (e.Target == want.Target || want.Target == "\x00") && - (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == "\x00") && - (slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == "\x00")) && - (e.FsType == want.FsType || want.FsType == "\x00") && - (e.Source == want.Source || want.Source == "\x00") && - (e.FsOptstr == want.FsOptstr || want.FsOptstr == "\x00") + (e.Root == want.Root || want.Root == ignore) && + (e.Target == want.Target || want.Target == ignore) && + (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && + (slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) && + (e.FsType == want.FsType || want.FsType == ignore) && + (e.Source == want.Source || want.Source == ignore) && + (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) } func (e *MountInfoEntry) String() string { diff --git a/sandbox/vfs/unfold.go b/sandbox/vfs/unfold.go new file mode 100644 index 00000000..ca0e37b7 --- /dev/null +++ b/sandbox/vfs/unfold.go @@ -0,0 +1,107 @@ +package vfs + +import ( + "iter" + "path" + "strings" + "syscall" +) + +// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. +type MountInfoNode struct { + *MountInfoEntry + FirstChild *MountInfoNode `json:"first_child"` + NextSibling *MountInfoNode `json:"next_sibling"` + + Clean string `json:"clean"` + Covered bool `json:"covered"` +} + +// Collective returns an iterator over visible mountinfo nodes. +func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] { + return func(yield func(*MountInfoNode) bool) { n.visit(yield) } +} + +func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool { + if !n.Covered && !yield(n) { + return false + } + for cur := n.FirstChild; cur != nil; cur = cur.NextSibling { + if !cur.visit(yield) { + return false + } + } + return true +} + +// Unfold unfolds the mount hierarchy and resolves covered paths. +func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) { + targetClean := path.Clean(target) + + var mountinfoSize int + for range d.Entries() { + mountinfoSize++ + } + if err := d.Err(); err != nil { + return nil, err + } + + mountinfo := make([]*MountInfoNode, mountinfoSize) + // mount ID to index lookup + idIndex := make(map[int]int, mountinfoSize) + // final entry to match target + targetIndex := -1 + { + i := 0 + for ent := range d.Entries() { + mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent} + idIndex[ent.ID] = i + if mountinfo[i].Clean == targetClean { + targetIndex = i + } + + i++ + } + } + + if targetIndex == -1 { + return nil, syscall.ESTALE + } + + for _, cur := range mountinfo { + var parent *MountInfoNode + if p, ok := idIndex[cur.Parent]; !ok { + continue + } else { + parent = mountinfo[p] + } + + if !strings.HasPrefix(cur.Clean, targetClean) { + continue + } + if parent.Clean == cur.Clean { + parent.Covered = true + } + + covered := false + nsp := &parent.FirstChild + for s := parent.FirstChild; s != nil; s = s.NextSibling { + if strings.HasPrefix(cur.Clean, s.Clean) { + covered = true + break + } + + if strings.HasPrefix(s.Clean, cur.Clean) { + *nsp = s.NextSibling + } else { + nsp = &s.NextSibling + } + } + if covered { + continue + } + *nsp = cur + } + + return mountinfo[targetIndex], nil +} diff --git a/sandbox/vfs/unfold_test.go b/sandbox/vfs/unfold_test.go new file mode 100644 index 00000000..4a262567 --- /dev/null +++ b/sandbox/vfs/unfold_test.go @@ -0,0 +1,93 @@ +package vfs_test + +import ( + "errors" + "reflect" + "slices" + "strings" + "syscall" + "testing" + + "git.gensokyo.uk/security/fortify/sandbox/vfs" +) + +func TestUnfold(t *testing.T) { + testCases := []struct { + name string + sample string + target string + wantErr error + + want *vfs.MountInfoNode + wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode + wantCollectN []string + }{ + { + "no match", + sampleMountinfoBase, + "/mnt", + syscall.ESTALE, nil, nil, nil, + }, + { + "cover", + `33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw +551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw +595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw +611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw +625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw +641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +`, "/mnt", nil, + mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false, + mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false, + mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true, + mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true, + mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false, + nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { + return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild} + }, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) + got, err := d.Unfold(tc.target) + + if !errors.Is(err, tc.wantErr) { + t.Errorf("Unfold: error = %v, wantErr %v", + err, tc.wantErr) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Unfold:\ngot %s\nwant %s", + mustMarshal(got), mustMarshal(tc.want)) + } + + if err == nil && tc.wantCollectF != nil { + t.Run("collective", func(t *testing.T) { + wantCollect := tc.wantCollectF(got) + gotCollect := slices.Collect(got.Collective()) + if !reflect.DeepEqual(gotCollect, wantCollect) { + t.Errorf("Collective: \ngot %#v\nwant %#v", + gotCollect, wantCollect) + } + t.Run("target", func(t *testing.T) { + gotCollectN := slices.Collect[string](func(yield func(v string) bool) { + for _, cur := range gotCollect { + if !yield(cur.Clean) { + return + } + } + }) + if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) { + t.Errorf("Collective: got %q, want %q", + gotCollectN, tc.wantCollectN) + } + }) + }) + } + }) + } +} -- cgit v1.3.1