From c7766a810a7d0022b952d3118e4c6992a94cca02 Mon Sep 17 00:00:00 2001 From: Anand-240 Date: Fri, 31 Jul 2026 17:44:18 +0530 Subject: [PATCH] fix(storage): unescape /proc/self/mountinfo paths in getMountInfo The kernel octal-escapes spaces, tabs, newlines and backslashes in the root and mount point fields of /proc/self/mountinfo. getMountInfo was comparing the raw, still-escaped mount point field against the caller's real path, so any bind-mount source containing one of those characters never matched and getBlockVolumes silently skipped it instead of attaching it as a block device. Switch to moby/sys/mountinfo.GetMounts, which already decodes the escaping, instead of hand-splitting the mountinfo lines. The matching logic is split into findMountInfo so it can be exercised with synthetic mountinfo entries in a test. Fixes: #867 Signed-off-by: Anand-240 --- pkg/unikontainers/block.go | 63 +++++++++++++++------------------ pkg/unikontainers/block_test.go | 33 +++++++++++++++++ 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/pkg/unikontainers/block.go b/pkg/unikontainers/block.go index 654f4a4ed..67da6d7e9 100644 --- a/pkg/unikontainers/block.go +++ b/pkg/unikontainers/block.go @@ -15,7 +15,6 @@ package unikontainers import ( - "bufio" "errors" "fmt" "os" @@ -50,7 +49,10 @@ type blockRootfs struct { } // getMountInfo determines whether the provided path is a mount point -// by inspecting /proc/self/mountinfo. +// by inspecting /proc/thread-self/mountinfo. As a result, it should be +// called only when all the threads of the process reside in the same +// mount namespace. Otherwise, the returned information depends on the +// thread which executed the function. // If the path is a mount point, it populates and returns a BlockDevParams struct. // Otherwise, it returns an error along with an empty BlockDevParams. // Additionally, when the path is a mount point, getMountInfo verifies @@ -59,54 +61,45 @@ type blockRootfs struct { // source device as the original mount, so they can appear identical to // regular mounts when inspecting mount information. func getMountInfo(path string) (types.BlockDevParams, error) { - selfProcMountInfo := "/proc/self/mountinfo" - - file, err := os.Open(selfProcMountInfo) + mountInfo, 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(mountInfo, path) +} + +// findMountInfo searches the parsed mountinfo entries for an entry mounted +// at path and, if it finds one, populates and returns a BlockDevParams +// struct out of it. It returns ErrMountpoint if no entry is mounted at path, +// or if the source of the matching entry is shared with another mount whose +// FS is not special. +func findMountInfo(mountInfo []*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 mountInfo { + 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) - // to restore them later in the delete path. - blockDev.MountOptions = preDash[5] + // Keep the mount VFS options, in order to + // restore them later in the delete path. + 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{}{} } } @@ -223,7 +216,7 @@ func getBlockVolumes(mounts []specs.Mount, ukernel types.Unikernel) ([]types.Blo continue } // Get the information of the source path - // from /proc/self/mountinfo + // from /proc/thread-self/mountinfo mInfo, err := getMountInfo(m.Source) if errors.Is(err, ErrMountpoint) { // ErrMountpoint means we did not find any diff --git a/pkg/unikontainers/block_test.go b/pkg/unikontainers/block_test.go index a33cd7eff..0f563e6a9 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, a tab or a backslash. The kernel octal-escapes these +// characters in mountinfo (e.g. a space becomes \040), so this makes sure +// that findMountInfo still matches the real, unescaped path against the +// parsed mountinfo entry. +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) + }) + } +}