Skip to content

Commit f5da9b8

Browse files
committed
merge #70 into cyphar/filepath-securejoin:main
Aleksa Sarai (3): pathrs-lite: rename procfs impl to purego suffix pathrs-lite: use black-box testing for API pathrs-lite: move purego implementation into subpackage LGTMs: cyphar
2 parents 82e47fb + 58e83f9 commit f5da9b8

File tree

15 files changed

+319
-138
lines changed

15 files changed

+319
-138
lines changed

pathrs-lite/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@
1111

1212
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
1313
// some of the APIs provided by [libpathrs].
14+
//
15+
// [libpathrs]: https://github.com/cyphar/libpathrs
1416
package pathrs
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build linux
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
// Package gopathrs is a less complete pure Go implementation of some of the
13+
// APIs provided by [libpathrs].
14+
//
15+
// [libpathrs]: https://github.com/cyphar/libpathrs
16+
package gopathrs

pathrs-lite/lookup_linux.go renamed to pathrs-lite/internal/gopathrs/lookup_linux.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// License, v. 2.0. If a copy of the MPL was not distributed with this
1010
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
1111

12-
package pathrs
12+
package gopathrs
1313

1414
import (
1515
"errors"
@@ -166,11 +166,11 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
166166
return tailEntry.dir, tailEntry.remainingPath, true
167167
}
168168

169-
// partialLookupInRoot tries to lookup as much of the request path as possible
169+
// PartialLookupInRoot tries to lookup as much of the request path as possible
170170
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
171171
// component of the requested path, returning a file handle to the final
172172
// existing component and a string containing the remaining path components.
173-
func partialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
173+
func PartialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
174174
return lookupInRoot(root, unsafePath, true)
175175
}
176176

pathrs-lite/lookup_linux_test.go renamed to pathrs-lite/internal/gopathrs/lookup_linux_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// License, v. 2.0. If a copy of the MPL was not distributed with this
1010
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
1111

12-
package pathrs
12+
package gopathrs
1313

1414
import (
1515
"errors"
@@ -312,7 +312,7 @@ func tRunWrapper(t *testing.T) testutils.TRunFunc {
312312
func TestPartialLookupInRoot(t *testing.T) {
313313
testutils.WithWithoutOpenat2(true, tRunWrapper(t), func(ti testutils.TestingT) {
314314
t := ti.(*testing.T) //nolint:forcetypeassert // guaranteed to be true and in test code
315-
testPartialLookup(t, partialLookupInRoot)
315+
testPartialLookup(t, PartialLookupInRoot)
316316
})
317317
}
318318

@@ -325,7 +325,7 @@ func TestPartialLookupInRoot_BadInode(t *testing.T) {
325325

326326
testutils.WithWithoutOpenat2(true, tRunWrapper(t), func(ti testutils.TestingT) {
327327
t := ti.(*testing.T) //nolint:forcetypeassert // guaranteed to be true and in test code
328-
partialLookupFn := partialLookupInRoot
328+
partialLookupFn := PartialLookupInRoot
329329

330330
tree := []string{
331331
// Make sure we don't open "bad" inodes.
@@ -384,7 +384,7 @@ func newRacingLookupMeta(pauseCh chan struct{}) *racingLookupMeta {
384384
func (m *racingLookupMeta) checkPartialLookup(t *testing.T, rootDir fd.Fd, unsafePath string, skipErrs []error, allowedResults []lookupResult) {
385385
// Similar to checkPartialLookup, but with extra logic for
386386
// handling the lookup stopping partly through the lookup.
387-
handle, remainingPath, err := partialLookupInRoot(rootDir, unsafePath)
387+
handle, remainingPath, err := PartialLookupInRoot(rootDir, unsafePath)
388388
var (
389389
handleName string
390390
realPath string
@@ -427,7 +427,7 @@ func (m *racingLookupMeta) checkPartialLookup(t *testing.T, rootDir fd.Fd, unsaf
427427
if realPath != handleName {
428428
// It's possible for handle.Name() to be wrong because while it was
429429
// correct when it was set, it might not match if the path was swapped
430-
// afterwards (for both openat2 and partialLookupInRoot).
430+
// afterwards (for both openat2 and PartialLookupInRoot).
431431
m.badNameCount++
432432
}
433433

@@ -571,7 +571,7 @@ func TestPartialLookup_RacingRename(t *testing.T) {
571571
// because there was a moment when this directory was inside
572572
// the root, and the attacker moved it outside the root.
573573
//
574-
// Neither openat2 nor partialLookupInRoot will allow us to
574+
// Neither openat2 nor PartialLookupInRoot will allow us to
575575
// walk into ".." in this case (escaping the root), and we
576576
// would catch that if it did happen.
577577
lookupResult{handlePath: "../outsideroot", remainingPath: "c/d/e", fileType: unix.S_IFDIR},

pathrs-lite/mkdir_linux.go renamed to pathrs-lite/internal/gopathrs/mkdir_linux.go

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// License, v. 2.0. If a copy of the MPL was not distributed with this
1010
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
1111

12-
package pathrs
12+
package gopathrs
1313

1414
import (
1515
"errors"
@@ -23,9 +23,12 @@ import (
2323
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
2424
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
2525
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
26+
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
2627
)
2728

28-
var errInvalidMode = errors.New("invalid permission mode")
29+
// ErrInvalidMode is returned from [MkdirAll] when the requested mode is
30+
// invalid.
31+
var ErrInvalidMode = errors.New("invalid permission mode")
2932

3033
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
3134
// and sticky bits.
@@ -45,11 +48,11 @@ func toUnixMode(mode os.FileMode) (uint32, error) {
4548
}
4649
// We don't allow file type bits.
4750
if mode&os.ModeType != 0 {
48-
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
51+
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", ErrInvalidMode, mode, mode)
4952
}
5053
// We don't allow other unknown modes.
5154
if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
52-
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
55+
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", ErrInvalidMode, mode, mode)
5356
}
5457
return sysMode, nil
5558
}
@@ -84,11 +87,11 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
8487
// users it seems more prudent to return an error so users notice that
8588
// these bits will not be set.
8689
if unixMode&^0o1777 != 0 {
87-
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
90+
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", ErrInvalidMode, mode)
8891
}
8992

9093
// Try to open as much of the path as possible.
91-
currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
94+
currentDir, remainingPath, err := PartialLookupInRoot(root, unsafePath)
9295
defer func() {
9396
if Err != nil {
9497
_ = currentDir.Close()
@@ -117,7 +120,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
117120
// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
118121
// always return a non-O_PATH handle). We also check that we actually got a
119122
// directory.
120-
if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
123+
if reopenDir, err := procfs.ReopenFd(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
121124
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
122125
} else if err != nil {
123126
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
@@ -207,40 +210,3 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
207210
}
208211
return currentDir, nil
209212
}
210-
211-
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
212-
// where the new directory is guaranteed to be within the root directory (if an
213-
// attacker can move directories from inside the root to outside the root, the
214-
// created directory tree might be outside of the root but the key constraint
215-
// is that at no point will we walk outside of the directory tree we are
216-
// creating).
217-
//
218-
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
219-
//
220-
// path, _ := securejoin.SecureJoin(root, unsafePath)
221-
// err := os.MkdirAll(path, mode)
222-
//
223-
// But is much safer. The above implementation is unsafe because if an attacker
224-
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
225-
// possible for MkdirAll to resolve unsafe symlink components and create
226-
// directories outside of the root.
227-
//
228-
// If you plan to open the directory after you have created it or want to use
229-
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
230-
// This function is a wrapper around [MkdirAllHandle].
231-
//
232-
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
233-
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
234-
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
235-
if err != nil {
236-
return err
237-
}
238-
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
239-
240-
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
241-
if err != nil {
242-
return err
243-
}
244-
_ = f.Close()
245-
return nil
246-
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build linux
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
package gopathrs_test
13+
14+
import (
15+
"fmt"
16+
"os"
17+
"testing"
18+
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
"golang.org/x/sys/unix"
22+
23+
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs"
24+
)
25+
26+
func TestMkdirAllHandle_InvalidMode(t *testing.T) { //nolint:revive // underscores are more readable for test helpers
27+
for _, test := range []struct {
28+
mode os.FileMode
29+
expectedErr error
30+
}{
31+
// unix.S_IS* bits are invalid.
32+
{unix.S_ISUID | 0o777, gopathrs.ErrInvalidMode},
33+
{unix.S_ISGID | 0o777, gopathrs.ErrInvalidMode},
34+
{unix.S_ISVTX | 0o777, gopathrs.ErrInvalidMode},
35+
{unix.S_ISUID | unix.S_ISGID | unix.S_ISVTX | 0o777, gopathrs.ErrInvalidMode},
36+
// unix.S_IFMT bits are also invalid.
37+
{unix.S_IFDIR | 0o777, gopathrs.ErrInvalidMode},
38+
{unix.S_IFREG | 0o777, gopathrs.ErrInvalidMode},
39+
{unix.S_IFIFO | 0o777, gopathrs.ErrInvalidMode},
40+
// os.FileType bits are also invalid.
41+
{os.ModeDir | 0o777, gopathrs.ErrInvalidMode},
42+
{os.ModeNamedPipe | 0o777, gopathrs.ErrInvalidMode},
43+
{os.ModeIrregular | 0o777, gopathrs.ErrInvalidMode},
44+
// suid/sgid bits are silently ignored by mkdirat and so we return an
45+
// error explicitly.
46+
{os.ModeSetuid | 0o777, gopathrs.ErrInvalidMode},
47+
{os.ModeSetgid | 0o777, gopathrs.ErrInvalidMode},
48+
{os.ModeSetuid | os.ModeSetgid | os.ModeSticky | 0o777, gopathrs.ErrInvalidMode},
49+
// Proper sticky bit should work.
50+
{os.ModeSticky | 0o777, nil},
51+
// Regular mode bits.
52+
{0o777, nil},
53+
{0o711, nil},
54+
} {
55+
test := test // copy iterator
56+
t.Run(fmt.Sprintf("%s.%.3o", test.mode, test.mode), func(t *testing.T) {
57+
root := t.TempDir()
58+
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
59+
require.NoError(t, err, "open root")
60+
defer rootDir.Close() //nolint:errcheck // test code
61+
62+
handle, err := gopathrs.MkdirAllHandle(rootDir, "a/b/c", test.mode)
63+
require.ErrorIsf(t, err, test.expectedErr, "mkdirall %.3o (%s)", test.mode, test.mode)
64+
if test.expectedErr == nil {
65+
assert.NotNil(t, handle, "returned handle should be non-nil")
66+
_ = handle.Close()
67+
} else {
68+
assert.Nil(t, handle, "returned handle should be nil")
69+
}
70+
})
71+
}
72+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build linux
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
package gopathrs
13+
14+
import (
15+
"os"
16+
)
17+
18+
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
19+
// using an *[os.File] handle, to ensure that the correct root directory is used.
20+
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
21+
handle, err := completeLookupInRoot(root, unsafePath)
22+
if err != nil {
23+
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
24+
}
25+
return handle, nil
26+
}

pathrs-lite/openat2_linux.go renamed to pathrs-lite/internal/gopathrs/openat2_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// License, v. 2.0. If a copy of the MPL was not distributed with this
1010
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
1111

12-
package pathrs
12+
package gopathrs
1313

1414
import (
1515
"errors"

pathrs-lite/mkdir.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build linux
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
package pathrs
13+
14+
import (
15+
"os"
16+
17+
"golang.org/x/sys/unix"
18+
)
19+
20+
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
21+
// where the new directory is guaranteed to be within the root directory (if an
22+
// attacker can move directories from inside the root to outside the root, the
23+
// created directory tree might be outside of the root but the key constraint
24+
// is that at no point will we walk outside of the directory tree we are
25+
// creating).
26+
//
27+
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
28+
//
29+
// path, _ := securejoin.SecureJoin(root, unsafePath)
30+
// err := os.MkdirAll(path, mode)
31+
//
32+
// But is much safer. The above implementation is unsafe because if an attacker
33+
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
34+
// possible for MkdirAll to resolve unsafe symlink components and create
35+
// directories outside of the root.
36+
//
37+
// If you plan to open the directory after you have created it or want to use
38+
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
39+
// This function is a wrapper around [MkdirAllHandle].
40+
//
41+
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
42+
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
43+
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
44+
if err != nil {
45+
return err
46+
}
47+
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
48+
49+
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
50+
if err != nil {
51+
return err
52+
}
53+
_ = f.Close()
54+
return nil
55+
}

0 commit comments

Comments
 (0)