Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 25 additions & 31 deletions pkg/unikontainers/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package unikontainers

import (
"bufio"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -58,55 +57,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.
Comment on lines +60 to +64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason for this comment.

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.
Comment on lines +74 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the comments aligned with the actual functionality of a function and not for unit tests or others.

func findMountInfo(mounts []*mountinfo.Info, path string) (types.BlockDevParams, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a different name for mounts (for example mountInfo).

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update this comment. It is stale with these changes.

// 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{}{}
}
}

Expand Down
33 changes: 33 additions & 0 deletions pkg/unikontainers/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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)
})
}
}
Loading