diff --git a/pkg/unikontainers/block.go b/pkg/unikontainers/block.go index 6914586c6..3c1df55eb 100644 --- a/pkg/unikontainers/block.go +++ b/pkg/unikontainers/block.go @@ -15,7 +15,6 @@ package unikontainers import ( - "bufio" "errors" "fmt" "os" @@ -56,55 +55,50 @@ type blockRootfs struct { // There are cases (e.g. bind mounts) where mounts use the same underlying // source device as the original mount, so they can appear identical to // regular mounts when inspecting mount information. +// +// We rely on moby/sys/mountinfo to parse /proc/self/mountinfo instead of +// splitting the raw lines ourselves, because the kernel octal-escapes +// spaces, tabs, newlines and backslashes in the root and mount point +// fields (see proc(5)), and mountinfo.GetMounts() already decodes them. func getMountInfo(path string) (types.BlockDevParams, error) { - selfProcMountInfo := "/proc/self/mountinfo" - - file, err := os.Open(selfProcMountInfo) + mounts, err := mountinfo.GetMounts(nil) if err != nil { - return types.BlockDevParams{}, fmt.Errorf("failed to open mountinfo: %w", err) + return types.BlockDevParams{}, fmt.Errorf("failed to read mountinfo: %w", err) } - defer file.Close() + return findMountInfo(mounts, path) +} + +// findMountInfo scans already-parsed mountinfo entries for the one mounted +// at path. It is split out from getMountInfo so the matching logic can be +// unit tested against synthetic mounts, without depending on the real +// /proc/self/mountinfo of the process running the test. +func findMountInfo(mounts []*mountinfo.Info, path string) (types.BlockDevParams, error) { blockDev := types.BlockDevParams{} nonSpecialSources := make(map[string]struct{}) - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, " - ") - if len(parts) != 2 { - return types.BlockDevParams{}, fmt.Errorf("invalid mountinfo line in /proc/self/mountinfo") - } - preDash := strings.Fields(parts[0]) - if len(preDash) < 6 { - continue - } - postDash := strings.Fields(parts[1]) - if len(postDash) < 2 { - continue - } - if preDash[4] == path { + for _, m := range mounts { + if m.Mountpoint == path { uniklog.WithFields(logrus.Fields{ "mounted at": path, - "device": postDash[1], - "fstype": postDash[0], - "options": preDash[5], + "device": m.Source, + "fstype": m.FSType, + "options": m.Options, }).Debug("Found block device") - blockDev.Source = postDash[1] - blockDev.FsType = postDash[0] + blockDev.Source = m.Source + blockDev.FsType = m.FSType blockDev.MountPoint = path - // Keep the-mount VFS options (field 6 of mountinfo) + // Keep the mount VFS options (field 6 of mountinfo) // to restore them later in the delete path. - blockDev.MountOptions = preDash[5] + blockDev.MountOptions = m.Options blockDev.ID = "" continue } // Store the source of all mounts with non-special fs // (e.g. overlay, tmpfs) in a map - if postDash[0] != postDash[1] { - nonSpecialSources[postDash[1]] = struct{}{} + if m.FSType != m.Source { + nonSpecialSources[m.Source] = struct{}{} } } diff --git a/pkg/unikontainers/block_test.go b/pkg/unikontainers/block_test.go index a33cd7eff..148b585aa 100644 --- a/pkg/unikontainers/block_test.go +++ b/pkg/unikontainers/block_test.go @@ -15,8 +15,10 @@ package unikontainers import ( + "strings" "testing" + "github.com/moby/sys/mountinfo" "github.com/stretchr/testify/assert" "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) @@ -37,3 +39,34 @@ func TestGetBlockDevice(t *testing.T) { assert.Equal(t, tmpMnt.FsType, rootFs.FsType, "Expected filesystem type to be proc") assert.Equal(t, tmpMnt.ID, rootFs.ID, "Expected ID to be empty") } + +// TestFindMountInfoEscapedPath reproduces a bind mount whose source path +// contains a space, tab, newline or backslash. The kernel octal-escapes +// these characters in /proc/self/mountinfo (e.g. a space becomes \040), so +// this exercises that findMountInfo still matches the real, unescaped path +// against the mountinfo entry once it has been parsed by mountinfo.GetMountsFromReader. +func TestFindMountInfoEscapedPath(t *testing.T) { + tests := []struct { + name string + path string + escapedRaw string + }{ + {name: "space", path: "/mnt/my volume", escapedRaw: `/mnt/my\040volume`}, + {name: "tab", path: "/mnt/my\tvolume", escapedRaw: `/mnt/my\011volume`}, + {name: "backslash", path: `/mnt/my\volume`, escapedRaw: `/mnt/my\134volume`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := "36 35 8:1 / " + tt.escapedRaw + " rw,relatime shared:1 - ext4 /dev/sdb1 rw" + mounts, err := mountinfo.GetMountsFromReader(strings.NewReader(line), nil) + assert.NoError(t, err, "expected the synthetic mountinfo line to parse") + + blockDev, err := findMountInfo(mounts, tt.path) + assert.NoError(t, err, "expected the escaped mount point to match the real path") + assert.Equal(t, "/dev/sdb1", blockDev.Source) + assert.Equal(t, "ext4", blockDev.FsType) + assert.Equal(t, tt.path, blockDev.MountPoint) + }) + } +}