pathrs: add "hallucination" helpers for SecureJoin magic

In order to maintain compatibility with previous releases of runc (which
permitted dangling symlinks as path components by permitting
non-existent path components to be treated like real directories) we
have to first do SecureJoin to construct a target path that is
compatible with the old behaviour but has all dangling symlinks (or
other invalid paths like ".." components after non-existent directories)
removed.

This is effectively a more generic verison of commit 3f925525b4
("rootfs: re-allow dangling symlinks in mount targets") and will let us
remove the need for open-coding SecureJoin workarounds.

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
(cherry picked from commit cfb74326be)
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
Aleksa Sarai
2025-11-09 01:41:10 +11:00
parent 7bf92b9d6a
commit da73ade8da
4 changed files with 40 additions and 25 deletions
+3 -13
View File
@@ -21,7 +21,6 @@ package pathrs
import (
"fmt"
"os"
"path/filepath"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/sirupsen/logrus"
@@ -50,18 +49,9 @@ import (
// needed for a lot of runc callers and fixing this would require reworking a
// lot of path logic).
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) {
// If the path is already "within" the root, get the path relative to the
// root and use that as the unsafe path. This is necessary because a lot of
// MkdirAllInRoot callers have already done SecureJoin, and refactoring
// all of them to stop using these SecureJoin'd paths would require a fair
// amount of work.
// TODO(cyphar): Do the refactor to libpathrs once it's ready.
if IsLexicallyInRoot(root, unsafePath) {
subPath, err := filepath.Rel(root, unsafePath)
if err != nil {
return nil, err
}
unsafePath = subPath
unsafePath, err := hallucinateUnsafePath(root, unsafePath)
if err != nil {
return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err)
}
// Check for any silly mode bits.
+32
View File
@@ -22,6 +22,8 @@ import (
"os"
"path/filepath"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
)
// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"),
@@ -82,3 +84,33 @@ func LexicallyStripRoot(root, path string) string {
}
return LexicallyCleanPath("/" + path)
}
// hallucinateUnsafePath creates a new unsafePath which has all symlinks
// (including dangling symlinks) fully resolved and any non-existent components
// treated as though they are real. This is effectively just a wrapper around
// [securejoin.SecureJoin] that strips the root. This path *IS NOT* safe to use
// as-is, you *MUST* operate on the returned path with pathrs-lite.
//
// The reason for this methods is that in previous runc versions, we would
// tolerate nonsense paths with dangling symlinks as path components.
// pathrs-lite does not support this, so instead we have to emulate this
// behaviour by doing SecureJoin *purely to get a semi-reasonable path to use*
// and then we use pathrs-lite to operate on the path safely.
//
// It would be quite difficult to emulate this in a race-free way in
// pathrs-lite, so instead we use [securejoin.SecureJoin] to simply produce a
// new candidate path for operations like [MkdirAllInRoot] so they can then
// operate on the new unsafePath as if it was what the user requested.
//
// If unsafePath is already lexically inside root, it is stripped before
// re-resolving it (this is done to ensure compatibility with legacy callers
// within runc that call SecureJoin before calling into pathrs).
func hallucinateUnsafePath(root, unsafePath string) (string, error) {
unsafePath = LexicallyStripRoot(root, unsafePath)
weirdPath, err := securejoin.SecureJoin(root, unsafePath)
if err != nil {
return "", err
}
unsafePath = LexicallyStripRoot(root, weirdPath)
return unsafePath, nil
}
+5
View File
@@ -50,6 +50,11 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) {
// include it in the passed flags. The fileMode argument uses unix.* mode bits,
// *not* os.FileMode.
func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) {
subpath, err := hallucinateUnsafePath(root, subpath)
if err != nil {
return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err)
}
dir, filename := filepath.Split(subpath)
if filepath.Join("/", filename) == "/" {
return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename)
-12
View File
@@ -546,18 +546,6 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
}
dstIsFile = !fi.IsDir()
}
// In previous runc versions, we would tolerate nonsense paths with
// dangling symlinks as path components. pathrs-lite does not support
// this, so instead we have to emulate this behaviour by doing
// SecureJoin *purely to get a semi-reasonable path to use* and then we
// use pathrs-lite to operate on the path safely.
newUnsafePath, err := securejoin.SecureJoin(rootfs, unsafePath)
if err != nil {
return err
}
unsafePath = pathrs.LexicallyStripRoot(rootfs, newUnsafePath)
if dstIsFile {
dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
} else {