Skip to content

Commit

Permalink
Support scanning paths in different mount namespaces via procfs
Browse files Browse the repository at this point in the history
Fixes anchore#3396

Signed-off-by: Ariel Miculas-Trif <[email protected]>
  • Loading branch information
ariel-miculas committed Nov 7, 2024
1 parent bc72963 commit 986a8a0
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 56 deletions.
122 changes: 98 additions & 24 deletions syft/internal/fileresolver/chroot_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/thediveo/procfsroot"
Expand All @@ -24,11 +25,34 @@ type ChrootContext struct {
}

func NewChrootContextFromCWD(root, base string) (*ChrootContext, error) {
currentWD, err := os.Getwd()
var currentWD string
var err error

cleanBase, err := NormalizeBaseDirectory(base)
if err != nil {
return nil, fmt.Errorf("could not get current working directory: %w", err)
return nil, err
}

inProcfs, err := isPathInProcfsPid(base)
if err != nil {
return nil, err
}

if inProcfs {
currentWD, err = getProcfsCwd(cleanBase)
if err != nil {
return nil, fmt.Errorf("could not get current working directory: %w", err)
}
log.Tracef("procfs cwd: %q", currentWD)
} else {
currentWD, err = os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not get current working directory: %w", err)
}
}

log.Tracef("procfs cwd: %q", currentWD)

return NewChrootContext(root, base, currentWD)
}

Expand Down Expand Up @@ -70,13 +94,9 @@ func EvalSymlinksRelativeToBase(source string, base string) (string, error) {
var path string
var resolvedPath string

if base == "" {
return filepath.EvalSymlinks(source)
}

// For windows we don't support resolving absolute symlinks inside a
// chroot, so we preserve the existing behavior
if windows.HostRunningOnWindows() {
if base == "" || windows.HostRunningOnWindows() {
return filepath.EvalSymlinks(source)
}

Expand All @@ -88,9 +108,19 @@ func EvalSymlinksRelativeToBase(source string, base string) (string, error) {
log.Tracef("solving source %q relative to base %q", source, base)
source = filepath.Clean(source)

// we don't support resolving relative paths when the base is a procfs path
inProcfs, err := isPathInProcfsPid(absBase)
if err != nil {
return "", err
}

if inProcfs && !filepath.IsAbs(source) {
return "", fmt.Errorf("relative paths are not supported with procfs base")
}

containedPaths := allContainedPaths(source)
for index, path = range containedPaths {
resolvedPath, err = filepath.EvalSymlinks(path)
resolvedPath, err = evalSymlinksExceptProcfs(path)
if err != nil {
return "", err
}
Expand All @@ -107,7 +137,7 @@ func EvalSymlinksRelativeToBase(source string, base string) (string, error) {
// if we don't encounter base, return the resolved path (which could be relative)
// note, the absolutePath is absolute, so we don't want to return that one
if !strings.HasPrefix(absPath, absBase) {
log.Tracef("resolved path = %s", resolvedPath)
log.Tracef("prefix not found, resolved path = %s", resolvedPath)
return resolvedPath, nil
}

Expand All @@ -125,12 +155,32 @@ func EvalSymlinksRelativeToBase(source string, base string) (string, error) {
}

log.Tracef("resolved path = %s", base+normalizedPath)

// we use base instead of absBase, since base could be relative
// it's the same argument as returning resolvedPath instead of absResolvedPath
return base + normalizedPath, nil
}

func getProcfsCwd(base string) (string, error) {
inProcfs, err := isPathInProcfsPid(base)
if err != nil {
return "", err
}
if !inProcfs {
return "", fmt.Errorf("path %q not in procfs", base)
}

components := strings.Split(base, "/")
pidStr := components[2]

processProcfsCwd := filepath.Join("/proc", pidStr, "cwd")
processProcfsCwd, err = os.Readlink(processProcfsCwd)
if err != nil {
return "", err
}
log.Tracef("base: %q, processProcfsCwd %q", base, processProcfsCwd)
return filepath.Join("/proc", pidStr, "root", processProcfsCwd), nil
}

func NormalizeRootDirectory(root string, base string) (string, error) {
cleanRoot, err := EvalSymlinksRelativeToBase(root, base)
if err != nil {
Expand All @@ -140,17 +190,52 @@ func NormalizeRootDirectory(root string, base string) (string, error) {
return cleanRoot, nil
}

func isPathInProcfsPid(path string) (bool, error) {
match, err := regexp.MatchString("/proc/[1-9][0-9]*/root", path)
if err != nil {
return false, err
}
return match, nil
}

// If both source and base are absolute we support base being a symlink
// This is mainly needed for procfs paths, e.g. /proc/PID/root, where
// PID could be in a different mount namespace, so we can't follow the
// symlink
func evalSymlinksExceptProcfs(path string) (string, error) {
// don't follow symlink for paths in procfs
inProcfs, err := isPathInProcfsPid(path)
if err != nil {
return "", err
}
if inProcfs {
return path, nil
}
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return "", fmt.Errorf("could not evaluate path=%q err: %w", path, err)
}
return resolvedPath, nil
}

func NormalizeBaseDirectory(base string) (string, error) {
var cleanBase string
var err error
if base == "" {
return "", nil
}

cleanBase, err := filepath.EvalSymlinks(base)
absBase, err := filepath.Abs(base)
if err != nil {
return "", err
}

cleanBase, err = evalSymlinksExceptProcfs(absBase)
if err != nil {
return "", fmt.Errorf("could not evaluate base=%q symlinks: %w", base, err)
}

return filepath.Abs(cleanBase)
return cleanBase, nil
}

// Root returns the root path with all symlinks evaluated.
Expand Down Expand Up @@ -236,18 +321,7 @@ func (r ChrootContext) ToNativeGlob(chrootPath string) (string, error) {
return r.ToNativePath(chrootPath)
}

responsePath := parts[0]

if filepath.IsAbs(responsePath) {
// don't allow input to potentially hop above root path
responsePath = path.Join(r.root, responsePath)
} else {
// ensure we take into account any relative difference between the root path and the CWD for relative requests
responsePath = path.Join(r.cwdRelativeToRoot, responsePath)
}

var err error
responsePath, err = filepath.Abs(responsePath)
responsePath, err := r.ToNativePath(parts[0])
if err != nil {
return "", err
}
Expand Down
30 changes: 29 additions & 1 deletion syft/internal/fileresolver/chroot_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fileresolver
import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -57,6 +58,11 @@ func Test_ChrootContext_RequestResponse(t *testing.T) {
absAbsToPathFromSomewhere := filepath.Join(absolute, "somewhere", "abs-to-path")
relAbsToPathFromSomewhere := filepath.Join(relative, "somewhere", "abs-to-path")

thisPid := os.Getpid()
processProcfsRoot := filepath.Join("/proc", strconv.Itoa(thisPid), "root")
processProcfsCwd, err := getProcfsCwd(processProcfsRoot)
assert.NoError(t, err)

cleanup := func() {
_ = os.Remove(absAbsInsidePath)
_ = os.Remove(absAbsOutsidePath)
Expand Down Expand Up @@ -502,12 +508,34 @@ func Test_ChrootContext_RequestResponse(t *testing.T) {
expectedNativePath: filepath.Join(absolute, "path", "to", "the", "file.txt"),
expectedChrootPath: "/to/the/file.txt",
},
{
name: "_procfs_, abs root, relative request, direct",
cwd: processProcfsCwd,
root: filepath.Join(processProcfsRoot, absolute),
base: processProcfsRoot,
input: "path/to/the/file.txt",
expectedNativePath: filepath.Join(processProcfsRoot, absolute, "path/to/the/file.txt"),
expectedChrootPath: filepath.Join(absolute, "path/to/the/file.txt"),
},
{
name: "_procfs_, abs root, abs request, direct",
root: filepath.Join(processProcfsRoot, absolute),
base: processProcfsRoot,
input: "/path/to/the/file.txt",
expectedNativePath: filepath.Join(processProcfsRoot, absolute, "path/to/the/file.txt"),
expectedChrootPath: filepath.Join(absolute, "path/to/the/file.txt"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {

var targetPath string
if filepath.IsAbs(c.cwd) {
targetPath = c.cwd
} else {
targetPath = filepath.Join(testDir, c.cwd)
}
// we need to mimic a shell, otherwise we won't get a path within a symlink
targetPath := filepath.Join(testDir, c.cwd)
t.Setenv("PWD", filepath.Clean(targetPath))

require.NoError(t, err)
Expand Down
85 changes: 54 additions & 31 deletions syft/internal/fileresolver/directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
"time"
Expand All @@ -21,6 +22,7 @@ import (
"go.uber.org/goleak"

stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
)

Expand Down Expand Up @@ -61,6 +63,12 @@ func TestDirectoryResolver_FilesByPath_request_response(t *testing.T) {
absAbsToPathFromSomewhere := filepath.Join(absolute, "somewhere", "abs-to-path")
relAbsToPathFromSomewhere := filepath.Join(relative, "somewhere", "abs-to-path")

thisPid := os.Getpid()
processProcfsRoot := filepath.Join("/proc", strconv.Itoa(thisPid), "root")
processProcfsCwd, err := getProcfsCwd(processProcfsRoot)
assert.NoError(t, err)
log.Tracef("cwd: %q", processProcfsCwd)

cleanup := func() {
_ = os.Remove(absInsidePath)
_ = os.Remove(absOutsidePath)
Expand Down Expand Up @@ -501,44 +509,54 @@ func TestDirectoryResolver_FilesByPath_request_response(t *testing.T) {
expectedAccessPath: "to/the/rel-outside.txt",
},
{
name: "absolute symlink to directory relative to the chroot",
root: chrootAbsSymlinkToDir,
base: absChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
expectedAccessPath: "/to/the/file.txt",
name: "absolute symlink to directory relative to the chroot",
root: chrootAbsSymlinkToDir,
base: absChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
},
{
name: "absolute symlink to directory relative to the chroot, relative root",
root: filepath.Join(relative, "path", "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
},
{
name: "absolute symlink to directory relative to the chroot, relative root",
root: filepath.Join(relative, "path", "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
expectedAccessPath: "/to/the/file.txt",
name: "absolute symlink to directory relative to the chroot, relative root via link",
root: filepath.Join(relativeViaLink, "path", "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
},
{
name: "absolute symlink to directory relative to the chroot, relative root via link",
root: filepath.Join(relativeViaLink, "path", "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
expectedAccessPath: "/to/the/file.txt",
name: "absolute symlink to directory relative to the chroot, with extra symlink to chroot",
root: filepath.Join(absAbsToPathFromSomewhere, "to", "chroot-abs-symlink-to-dir"),
base: absChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
},
{
name: "absolute symlink to directory relative to the chroot, with extra symlink to chroot",
root: filepath.Join(absAbsToPathFromSomewhere, "to", "chroot-abs-symlink-to-dir"),
base: absChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
expectedAccessPath: "/to/the/file.txt",
name: "absolute symlink to directory relative to the chroot, with extra symlink to chroot, relative root",
root: filepath.Join(relAbsToPathFromSomewhere, "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
},
{
name: "absolute symlink to directory relative to the chroot, with extra symlink to chroot, relative root",
root: filepath.Join(relAbsToPathFromSomewhere, "to", "chroot-abs-symlink-to-dir"),
base: relChrootBase,
input: "file.txt",
expectedRealPath: "/to/the/file.txt",
expectedAccessPath: "/to/the/file.txt",
name: "_procfs_, abs root, relative request, direct",
cwd: processProcfsCwd,
root: filepath.Join(processProcfsRoot, absolute),
base: processProcfsRoot,
input: "path/to/the/file.txt",
expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"),
},
{
name: "_procfs_, abs root, abs request, direct",
root: filepath.Join(processProcfsRoot, absolute),
base: processProcfsRoot,
input: "/path/to/the/file.txt",
expectedRealPath: filepath.Join(absolute, "path/to/the/file.txt"),
},
}
for _, c := range cases {
Expand All @@ -547,8 +565,13 @@ func TestDirectoryResolver_FilesByPath_request_response(t *testing.T) {
c.expectedAccessPath = c.expectedRealPath
}

var targetPath string
if filepath.IsAbs(c.cwd) {
targetPath = c.cwd
} else {
targetPath = filepath.Join(testDir, c.cwd)
}
// we need to mimic a shell, otherwise we won't get a path within a symlink
targetPath := filepath.Join(testDir, c.cwd)
t.Setenv("PWD", filepath.Clean(targetPath))

require.NoError(t, err)
Expand Down
7 changes: 7 additions & 0 deletions syft/internal/fileresolver/path_skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ func newPathSkipperFromMounts(root string, infos []*mountinfo.Info) pathSkipper
"tmpfs": {"/run", "/dev", "/var/run", "/var/lock", "/sys"},
}

inProcfs, err := isPathInProcfsPid(root)
if err == nil && inProcfs {
log.Debugf("Including procfs mount types because root %q is in procfs", root)
delete(ignorableMountTypes, "proc")
delete(ignorableMountTypes, "procfs")
}

// The longest path is the most specific path, e.g.
// if / is mounted as tmpfs, but /home/syft/permanent is mounted as ext4,
// then the mount type for /home/syft/permanent/foo is ext4, and the mount info
Expand Down
Loading

0 comments on commit 986a8a0

Please sign in to comment.