Skip to content

Commit 42a1e19

Browse files
committed
libcontainer: move CleanPath and StripRoot to internal/pathrs
These helpers will be needed for the compatibility code added in future patches in this series, but because "internal/pathrs" is imported by "libcontainer/utils" we need to move them so that we can avoid circular dependencies. Because the old functions were in a non-internal package it is possible some downstreams use them, so add some wrappers but mark them as deprecated. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 475473d commit 42a1e19

File tree

9 files changed

+159
-136
lines changed

9 files changed

+159
-136
lines changed

internal/pathrs/path.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package pathrs
2020

2121
import (
22+
"os"
23+
"path/filepath"
2224
"strings"
2325
)
2426

@@ -32,3 +34,51 @@ func IsLexicallyInRoot(root, path string) bool {
3234
path = strings.TrimRight(path, "/")
3335
return strings.HasPrefix(path+"/", root+"/")
3436
}
37+
38+
// LexicallyCleanPath makes a path safe for use with filepath.Join. This is
39+
// done by not only cleaning the path, but also (if the path is relative)
40+
// adding a leading '/' and cleaning it (then removing the leading '/'). This
41+
// ensures that a path resulting from prepending another path will always
42+
// resolve to lexically be a subdirectory of the prefixed path. This is all
43+
// done lexically, so paths that include symlinks won't be safe as a result of
44+
// using CleanPath.
45+
func LexicallyCleanPath(path string) string {
46+
// Deal with empty strings nicely.
47+
if path == "" {
48+
return ""
49+
}
50+
51+
// Ensure that all paths are cleaned (especially problematic ones like
52+
// "/../../../../../" which can cause lots of issues).
53+
54+
if filepath.IsAbs(path) {
55+
return filepath.Clean(path)
56+
}
57+
58+
// If the path isn't absolute, we need to do more processing to fix paths
59+
// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
60+
// paths to relative ones.
61+
path = filepath.Clean(string(os.PathSeparator) + path)
62+
// This can't fail, as (by definition) all paths are relative to root.
63+
path, _ = filepath.Rel(string(os.PathSeparator), path)
64+
65+
return path
66+
}
67+
68+
// LexicallyStripRoot returns the passed path, stripping the root path if it
69+
// was (lexicially) inside it. Note that both passed paths will always be
70+
// treated as absolute, and the returned path will also always be absolute. In
71+
// addition, the paths are cleaned before stripping the root.
72+
func LexicallyStripRoot(root, path string) string {
73+
// Make the paths clean and absolute.
74+
root, path = LexicallyCleanPath("/"+root), LexicallyCleanPath("/"+path)
75+
switch {
76+
case path == root:
77+
path = "/"
78+
case root == "/":
79+
// do nothing
80+
default:
81+
path = strings.TrimPrefix(path, root+"/")
82+
}
83+
return LexicallyCleanPath("/" + path)
84+
}

internal/pathrs/path_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,70 @@ func TestIsLexicallyInRoot(t *testing.T) {
5151
})
5252
}
5353
}
54+
55+
func TestLexicallyCleanPath(t *testing.T) {
56+
path := LexicallyCleanPath("")
57+
if path != "" {
58+
t.Errorf("expected to receive empty string and received %s", path)
59+
}
60+
61+
path = LexicallyCleanPath("rootfs")
62+
if path != "rootfs" {
63+
t.Errorf("expected to receive 'rootfs' and received %s", path)
64+
}
65+
66+
path = LexicallyCleanPath("../../../var")
67+
if path != "var" {
68+
t.Errorf("expected to receive 'var' and received %s", path)
69+
}
70+
71+
path = LexicallyCleanPath("/../../../var")
72+
if path != "/var" {
73+
t.Errorf("expected to receive '/var' and received %s", path)
74+
}
75+
76+
path = LexicallyCleanPath("/foo/bar/")
77+
if path != "/foo/bar" {
78+
t.Errorf("expected to receive '/foo/bar' and received %s", path)
79+
}
80+
81+
path = LexicallyCleanPath("/foo/bar/../")
82+
if path != "/foo" {
83+
t.Errorf("expected to receive '/foo' and received %s", path)
84+
}
85+
}
86+
87+
func TestLexicallyStripRoot(t *testing.T) {
88+
for _, test := range []struct {
89+
root, path, out string
90+
}{
91+
// Works with multiple components.
92+
{"/a/b", "/a/b/c", "/c"},
93+
{"/hello/world", "/hello/world/the/quick-brown/fox", "/the/quick-brown/fox"},
94+
// '/' must be a no-op.
95+
{"/", "/a/b/c", "/a/b/c"},
96+
// Must be the correct order.
97+
{"/a/b", "/a/c/b", "/a/c/b"},
98+
// Must be at start.
99+
{"/abc/def", "/foo/abc/def/bar", "/foo/abc/def/bar"},
100+
// Must be a lexical parent.
101+
{"/foo/bar", "/foo/barSAMECOMPONENT", "/foo/barSAMECOMPONENT"},
102+
// Must only strip the root once.
103+
{"/foo/bar", "/foo/bar/foo/bar/baz", "/foo/bar/baz"},
104+
// Deal with .. in a fairly sane way.
105+
{"/foo/bar", "/foo/bar/../baz", "/foo/baz"},
106+
{"/foo/bar", "../../../../../../foo/bar/baz", "/baz"},
107+
{"/foo/bar", "/../../../../../../foo/bar/baz", "/baz"},
108+
{"/foo/bar/../baz", "/foo/baz/bar", "/bar"},
109+
{"/foo/bar/../baz", "/foo/baz/../bar/../baz/./foo", "/foo"},
110+
// All paths are made absolute before stripping.
111+
{"foo/bar", "/foo/bar/baz/bee", "/baz/bee"},
112+
{"/foo/bar", "foo/bar/baz/beef", "/baz/beef"},
113+
{"foo/bar", "foo/bar/baz/beets", "/baz/beets"},
114+
} {
115+
got := LexicallyStripRoot(test.root, test.path)
116+
if got != test.out {
117+
t.Errorf("LexicallyStripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out)
118+
}
119+
}
120+
}

libcontainer/apparmor/apparmor_linux.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"golang.org/x/sys/unix"
1010

1111
"github.com/opencontainers/runc/internal/pathrs"
12-
"github.com/opencontainers/runc/libcontainer/utils"
1312
)
1413

1514
var (
@@ -29,7 +28,7 @@ func isEnabled() bool {
2928
}
3029

3130
func setProcAttr(attr, value string) error {
32-
attr = utils.CleanPath(attr)
31+
attr = pathrs.LexicallyCleanPath(attr)
3332
attrSubPath := "attr/apparmor/" + attr
3433
if _, err := os.Stat("/proc/self/" + attrSubPath); errors.Is(err, os.ErrNotExist) {
3534
// fall back to the old convention

libcontainer/factory_linux.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import (
1111

1212
"github.com/opencontainers/cgroups"
1313
"github.com/opencontainers/cgroups/manager"
14+
"github.com/opencontainers/runc/internal/pathrs"
1415
"github.com/opencontainers/runc/libcontainer/configs"
1516
"github.com/opencontainers/runc/libcontainer/configs/validate"
1617
"github.com/opencontainers/runc/libcontainer/intelrdt"
17-
"github.com/opencontainers/runc/libcontainer/utils"
1818
)
1919

2020
const (
@@ -211,7 +211,7 @@ func validateID(id string) error {
211211

212212
}
213213

214-
if string(os.PathSeparator)+id != utils.CleanPath(string(os.PathSeparator)+id) {
214+
if string(os.PathSeparator)+id != pathrs.LexicallyCleanPath(string(os.PathSeparator)+id) {
215215
return ErrInvalidID
216216
}
217217

libcontainer/rootfs_linux.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) {
9191
// needsSetupDev returns true if /dev needs to be set up.
9292
func needsSetupDev(config *configs.Config) bool {
9393
for _, m := range config.Mounts {
94-
if m.Device == "bind" && utils.CleanPath(m.Destination) == "/dev" {
94+
if m.Device == "bind" && pathrs.LexicallyCleanPath(m.Destination) == "/dev" {
9595
return false
9696
}
9797
}
@@ -257,7 +257,7 @@ func finalizeRootfs(config *configs.Config) (err error) {
257257
if m.Flags&unix.MS_RDONLY != unix.MS_RDONLY {
258258
continue
259259
}
260-
if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" {
260+
if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" {
261261
if err := remountReadonly(m); err != nil {
262262
return err
263263
}
@@ -506,7 +506,7 @@ func statfsToMountFlags(st unix.Statfs_t) int {
506506
var errRootfsToFile = errors.New("config tries to change rootfs to file")
507507

508508
func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
509-
unsafePath := utils.StripRoot(rootfs, m.Destination)
509+
unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination)
510510
dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH)
511511
defer func() {
512512
if dstFile != nil && Err != nil {
@@ -553,7 +553,7 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
553553
if err != nil {
554554
return err
555555
}
556-
unsafePath = utils.StripRoot(rootfs, newUnsafePath)
556+
unsafePath = pathrs.LexicallyStripRoot(rootfs, newUnsafePath)
557557

558558
if dstIsFile {
559559
dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
@@ -952,7 +952,7 @@ func createDevices(config *configs.Config) error {
952952
for _, node := range config.Devices {
953953

954954
// The /dev/ptmx device is setup by setupPtmx()
955-
if utils.CleanPath(node.Path) == "/dev/ptmx" {
955+
if pathrs.LexicallyCleanPath(node.Path) == "/dev/ptmx" {
956956
continue
957957
}
958958

@@ -1392,7 +1392,7 @@ func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err err
13921392
if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
13931393
return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
13941394
}
1395-
unsafePath := utils.StripRoot(rootfs, fullPath)
1395+
unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath)
13961396
reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags)
13971397
if err != nil {
13981398
return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err)
@@ -1432,7 +1432,7 @@ func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error {
14321432
// operations on it. We need to set up files in "/dev", and other tmpfs
14331433
// mounts may need to be chmod-ed after mounting. These mounts will be
14341434
// remounted ro later in finalizeRootfs(), if necessary.
1435-
if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" {
1435+
if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" {
14361436
flags &= ^unix.MS_RDONLY
14371437
}
14381438

libcontainer/specconv/spec_linux.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ import (
1616

1717
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
1818
dbus "github.com/godbus/dbus/v5"
19+
"github.com/opencontainers/runtime-spec/specs-go"
20+
"github.com/sirupsen/logrus"
21+
"golang.org/x/sys/unix"
22+
1923
"github.com/opencontainers/cgroups"
2024
devices "github.com/opencontainers/cgroups/devices/config"
2125
"github.com/opencontainers/runc/internal/linux"
26+
"github.com/opencontainers/runc/internal/pathrs"
2227
"github.com/opencontainers/runc/libcontainer/configs"
2328
"github.com/opencontainers/runc/libcontainer/internal/userns"
2429
"github.com/opencontainers/runc/libcontainer/seccomp"
25-
libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils"
26-
"github.com/opencontainers/runtime-spec/specs-go"
27-
"github.com/sirupsen/logrus"
28-
29-
"golang.org/x/sys/unix"
3030
)
3131

3232
var (
@@ -802,7 +802,7 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*cgrou
802802
if useSystemdCgroup {
803803
myCgroupPath = spec.Linux.CgroupsPath
804804
} else {
805-
myCgroupPath = libcontainerUtils.CleanPath(spec.Linux.CgroupsPath)
805+
myCgroupPath = pathrs.LexicallyCleanPath(spec.Linux.CgroupsPath)
806806
}
807807
}
808808

libcontainer/utils/utils.go

Lines changed: 22 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ package utils
44
import (
55
"encoding/json"
66
"io"
7-
"os"
8-
"path/filepath"
97
"strings"
108

119
"golang.org/x/sys/unix"
10+
11+
"github.com/opencontainers/runc/internal/pathrs"
1212
)
1313

1414
const (
@@ -37,53 +37,6 @@ func WriteJSON(w io.Writer, v any) error {
3737
return err
3838
}
3939

40-
// CleanPath makes a path safe for use with filepath.Join. This is done by not
41-
// only cleaning the path, but also (if the path is relative) adding a leading
42-
// '/' and cleaning it (then removing the leading '/'). This ensures that a
43-
// path resulting from prepending another path will always resolve to lexically
44-
// be a subdirectory of the prefixed path. This is all done lexically, so paths
45-
// that include symlinks won't be safe as a result of using CleanPath.
46-
func CleanPath(path string) string {
47-
// Deal with empty strings nicely.
48-
if path == "" {
49-
return ""
50-
}
51-
52-
// Ensure that all paths are cleaned (especially problematic ones like
53-
// "/../../../../../" which can cause lots of issues).
54-
55-
if filepath.IsAbs(path) {
56-
return filepath.Clean(path)
57-
}
58-
59-
// If the path isn't absolute, we need to do more processing to fix paths
60-
// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
61-
// paths to relative ones.
62-
path = filepath.Clean(string(os.PathSeparator) + path)
63-
// This can't fail, as (by definition) all paths are relative to root.
64-
path, _ = filepath.Rel(string(os.PathSeparator), path)
65-
66-
return path
67-
}
68-
69-
// StripRoot returns the passed path, stripping the root path if it was
70-
// (lexicially) inside it. Note that both passed paths will always be treated
71-
// as absolute, and the returned path will also always be absolute. In
72-
// addition, the paths are cleaned before stripping the root.
73-
func StripRoot(root, path string) string {
74-
// Make the paths clean and absolute.
75-
root, path = CleanPath("/"+root), CleanPath("/"+path)
76-
switch {
77-
case path == root:
78-
path = "/"
79-
case root == "/":
80-
// do nothing
81-
default:
82-
path = strings.TrimPrefix(path, root+"/")
83-
}
84-
return CleanPath("/" + path)
85-
}
86-
8740
// SearchLabels searches through a list of key=value pairs for a given key,
8841
// returning its value, and the binary flag telling whether the key exist.
8942
func SearchLabels(labels []string, key string) (string, bool) {
@@ -114,3 +67,23 @@ func Annotations(labels []string) (bundle string, userAnnotations map[string]str
11467
}
11568
return bundle, userAnnotations
11669
}
70+
71+
// CleanPath makes a path safe for use with filepath.Join. This is done by not
72+
// only cleaning the path, but also (if the path is relative) adding a leading
73+
// '/' and cleaning it (then removing the leading '/'). This ensures that a
74+
// path resulting from prepending another path will always resolve to lexically
75+
// be a subdirectory of the prefixed path. This is all done lexically, so paths
76+
// that include symlinks won't be safe as a result of using CleanPath.
77+
//
78+
// Deprecated: This function has been moved to internal/pathrs and this wrapper
79+
// will be removed in runc 1.5.
80+
var CleanPath = pathrs.LexicallyCleanPath
81+
82+
// StripRoot returns the passed path, stripping the root path if it was
83+
// (lexicially) inside it. Note that both passed paths will always be treated
84+
// as absolute, and the returned path will also always be absolute. In
85+
// addition, the paths are cleaned before stripping the root.
86+
//
87+
// Deprecated: This function has been moved to internal/pathrs and this wrapper
88+
// will be removed in runc 1.5.
89+
var StripRoot = pathrs.LexicallyStripRoot

0 commit comments

Comments
 (0)