Skip to content

Commit 0c2fbe6

Browse files
committed
mkdirall: switch to os.FileMode argument
This is mostly a cosmetic change for most users, but libpathrs uses os.FileMode as well and most Go users are more used to using os.FileMode. The only thing that users need to watch out for is that they need to switch from unix.S_ISVTX to os.ModeSticky if they are using that bit (since os.FileMode and unix.S_* bits have a different layout). This will also help with building on 32-bit architectures without switching the argument type to uint32 explicitly. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent f3a512c commit 0c2fbe6

File tree

3 files changed

+69
-29
lines changed

3 files changed

+69
-29
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2222
necessarily a breaking API change (though we expect no real users to be
2323
affected by it).
2424

25+
- `MkdirAll` and `MkdirHandle` now take an `os.FileMode`-style mode argument
26+
instead of a raw `unix.S_*`-style mode argument, which may cause compile-time
27+
type errors depending on how you use `filepath-securejoin`. For most users,
28+
there will be no change in behaviour aside from the type change (as the
29+
bottom `0o777` bits are the same in both formats, and most users are probably
30+
only using those bits).
31+
32+
However, if you were using `unix.S_ISVTX` to set the sticky bit with
33+
`MkdirAll(Handle)` you will need to switch to `os.ModeSticky` otherwise you
34+
will get a runtime error with this update. In addition, the error message you
35+
will get from passing `unix.S_ISUID` and `unix.S_ISGID` will be different as
36+
they are treated as invalid bits now (note that previously passing said bits
37+
was also an error).
38+
2539
## [0.3.6] - 2024-12-17 ##
2640

2741
### Compatibility ###

mkdir_linux.go

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ var (
2121
errPossibleAttack = errors.New("possible attack detected")
2222
)
2323

24+
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
25+
// and sticky bits.
26+
const modePermExt = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
27+
28+
//nolint:cyclop // this function needs to handle a lot of cases
29+
func toUnixMode(mode os.FileMode) (uint32, error) {
30+
sysMode := uint32(mode.Perm())
31+
if mode&os.ModeSetuid != 0 {
32+
sysMode |= unix.S_ISUID
33+
}
34+
if mode&os.ModeSetgid != 0 {
35+
sysMode |= unix.S_ISGID
36+
}
37+
if mode&os.ModeSticky != 0 {
38+
sysMode |= unix.S_ISVTX
39+
}
40+
// We don't allow file type bits.
41+
if mode&os.ModeType != 0 {
42+
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
43+
}
44+
// We don't allow other unknown modes.
45+
if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
46+
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
47+
}
48+
return sysMode, nil
49+
}
50+
2451
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
2552
// in two respects:
2653
//
@@ -39,17 +66,17 @@ var (
3966
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
4067
// doing [MkdirAll]. If you intend to open the directory after creating it, you
4168
// should use MkdirAllHandle.
42-
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err error) {
43-
// Make sure there are no os.FileMode bits set.
44-
if mode&^0o7777 != 0 {
45-
return nil, fmt.Errorf("%w for mkdir 0o%.3o", errInvalidMode, mode)
69+
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
70+
unixMode, err := toUnixMode(mode)
71+
if err != nil {
72+
return nil, err
4673
}
4774
// On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid
4875
// bits. We could also silently ignore them but since we have very few
4976
// users it seems more prudent to return an error so users notice that
5077
// these bits will not be set.
51-
if mode&^0o1777 != 0 {
52-
return nil, fmt.Errorf("%w for mkdir 0o%.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
78+
if unixMode&^0o1777 != 0 {
79+
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
5380
}
5481

5582
// Try to open as much of the path as possible.
@@ -104,9 +131,6 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err
104131
return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath)
105132
}
106133

107-
// Make sure the mode doesn't have any type bits.
108-
mode &^= unix.S_IFMT
109-
110134
// Create the remaining components.
111135
for _, part := range remainingParts {
112136
switch part {
@@ -123,7 +147,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err
123147
// directory at the same time as us. In that case, just continue on as
124148
// if we created it (if the created inode is not a directory, the
125149
// following open call will fail).
126-
if err := unix.Mkdirat(int(currentDir.Fd()), part, uint32(mode)); err != nil && !errors.Is(err, unix.EEXIST) {
150+
if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
127151
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
128152
// Make the error a bit nicer if the directory is dead.
129153
if deadErr := isDeadInode(currentDir); deadErr != nil {
@@ -196,10 +220,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err
196220
// If you plan to open the directory after you have created it or want to use
197221
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
198222
// This function is a wrapper around [MkdirAllHandle].
199-
//
200-
// NOTE: The mode argument must be set the unix mode bits (unix.S_I...), not
201-
// the Go generic mode bits ([os.FileMode]...).
202-
func MkdirAll(root, unsafePath string, mode int) error {
223+
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
203224
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
204225
if err != nil {
205226
return err

mkdir_linux_test.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import (
2020
"golang.org/x/sys/unix"
2121
)
2222

23-
type mkdirAllFunc func(t *testing.T, root, unsafePath string, mode int) error
23+
type mkdirAllFunc func(t *testing.T, root, unsafePath string, mode os.FileMode) error
2424

25-
var mkdirAll_MkdirAll mkdirAllFunc = func(t *testing.T, root, unsafePath string, mode int) error {
25+
var mkdirAll_MkdirAll mkdirAllFunc = func(t *testing.T, root, unsafePath string, mode os.FileMode) error {
2626
// We can't check expectedPath here.
2727
return MkdirAll(root, unsafePath, mode)
2828
}
2929

30-
var mkdirAll_MkdirAllHandle mkdirAllFunc = func(t *testing.T, root, unsafePath string, mode int) error {
30+
var mkdirAll_MkdirAllHandle mkdirAllFunc = func(t *testing.T, root, unsafePath string, mode os.FileMode) error {
3131
// Same logic as MkdirAll.
3232
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
3333
if err != nil {
@@ -55,7 +55,7 @@ var mkdirAll_MkdirAllHandle mkdirAllFunc = func(t *testing.T, root, unsafePath s
5555
return nil
5656
}
5757

58-
func checkMkdirAll(t *testing.T, mkdirAll mkdirAllFunc, root, unsafePath string, mode, expectedMode int, expectedErr error) {
58+
func checkMkdirAll(t *testing.T, mkdirAll mkdirAllFunc, root, unsafePath string, mode os.FileMode, expectedMode int, expectedErr error) {
5959
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
6060
require.NoError(t, err)
6161
defer rootDir.Close()
@@ -248,31 +248,36 @@ func TestMkdirAllHandle_AsRoot(t *testing.T) {
248248

249249
func testMkdirAll_InvalidMode(t *testing.T, mkdirAll mkdirAllFunc) {
250250
for _, test := range []struct {
251-
mode int
251+
mode os.FileMode
252252
expectedErr error
253253
}{
254-
// os.FileMode bits are invalid.
255-
{int(os.ModeDir | 0o777), errInvalidMode},
256-
{int(os.ModeSticky | 0o777), errInvalidMode},
257-
{int(os.ModeIrregular | 0o777), errInvalidMode},
254+
// unix.S_IS* bits are invalid.
255+
{unix.S_ISUID | 0o777, errInvalidMode},
256+
{unix.S_ISGID | 0o777, errInvalidMode},
257+
{unix.S_ISVTX | 0o777, errInvalidMode},
258+
{unix.S_ISUID | unix.S_ISGID | unix.S_ISVTX | 0o777, errInvalidMode},
258259
// unix.S_IFMT bits are also invalid.
259260
{unix.S_IFDIR | 0o777, errInvalidMode},
260261
{unix.S_IFREG | 0o777, errInvalidMode},
261262
{unix.S_IFIFO | 0o777, errInvalidMode},
263+
// os.FileType bits are also invalid.
264+
{os.ModeDir | 0o777, errInvalidMode},
265+
{os.ModeNamedPipe | 0o777, errInvalidMode},
266+
{os.ModeIrregular | 0o777, errInvalidMode},
262267
// suid/sgid bits are silently ignored by mkdirat and so we return an
263268
// error explicitly.
264-
{unix.S_ISUID | 0o777, errInvalidMode},
265-
{unix.S_ISGID | 0o777, errInvalidMode},
266-
{unix.S_ISUID | unix.S_ISGID | unix.S_ISVTX | 0o777, errInvalidMode},
269+
{os.ModeSetuid | 0o777, errInvalidMode},
270+
{os.ModeSetgid | 0o777, errInvalidMode},
271+
{os.ModeSetuid | os.ModeSetgid | os.ModeSticky | 0o777, errInvalidMode},
267272
// Proper sticky bit should work.
268-
{unix.S_ISVTX | 0o777, nil},
273+
{os.ModeSticky | 0o777, nil},
269274
// Regular mode bits.
270275
{0o777, nil},
271276
{0o711, nil},
272277
} {
273278
root := t.TempDir()
274279
err := mkdirAll(t, root, "a/b/c", test.mode)
275-
assert.ErrorIsf(t, err, test.expectedErr, "mkdirall 0o%.3o", test.mode)
280+
assert.ErrorIsf(t, err, test.expectedErr, "mkdirall %+.3o (%s)", test.mode, test.mode)
276281
}
277282
}
278283

@@ -295,7 +300,7 @@ func newRacingMkdirMeta() *racingMkdirMeta {
295300
}
296301
}
297302

298-
func (m *racingMkdirMeta) checkMkdirAllHandle_Racing(t *testing.T, root, unsafePath string, mode int, allowedErrs []error) {
303+
func (m *racingMkdirMeta) checkMkdirAllHandle_Racing(t *testing.T, root, unsafePath string, mode os.FileMode, allowedErrs []error) {
299304
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
300305
require.NoError(t, err, "open root")
301306
defer rootDir.Close()

0 commit comments

Comments
 (0)