From 46f5a2647155fb37e585e34c187a90650889c2dd Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 23 Jul 2024 15:26:31 +1000 Subject: [PATCH 1/5] test mocks: procfs: make unsafe fallback more realistic It makes more sense to make the open("/proc") unsafe fallback more like a hasNewMountApi() failure. Signed-off-by: Aleksa Sarai --- procfs_linux.go | 4 ++-- testing_mocks_linux.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/procfs_linux.go b/procfs_linux.go index daac3f0..3c08285 100644 --- a/procfs_linux.go +++ b/procfs_linux.go @@ -160,7 +160,7 @@ func clonePrivateProcMount() (_ *os.File, Err error) { } func privateProcRoot() (*os.File, error) { - if !hasNewMountApi() { + if !hasNewMountApi() || testingForceGetProcRootUnsafe() { return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) } // Try to create a new procfs mount from scratch if we can. This ensures we @@ -199,7 +199,7 @@ func unsafeHostProcRoot() (_ *os.File, Err error) { func doGetProcRoot() (*os.File, error) { procRoot, err := privateProcRoot() - if err != nil || testingForceGetProcRootUnsafe(procRoot) { + if err != nil { // Fall back to using a /proc handle if making a private mount failed. // If we have openat2, at least we can avoid some kinds of over-mount // attacks, but without openat2 there's not much we can do. diff --git a/testing_mocks_linux.go b/testing_mocks_linux.go index 2a25d08..a3aedf0 100644 --- a/testing_mocks_linux.go +++ b/testing_mocks_linux.go @@ -42,9 +42,9 @@ func testingForcePrivateProcRootOpenTreeAtRecursive(f *os.File) bool { testingCheckClose(*testingForceGetProcRoot >= forceGetProcRootOpenTreeAtRecursive, f) } -func testingForceGetProcRootUnsafe(f *os.File) bool { +func testingForceGetProcRootUnsafe() bool { return testing.Testing() && testingForceGetProcRoot != nil && - testingCheckClose(*testingForceGetProcRoot >= forceGetProcRootUnsafe, f) + *testingForceGetProcRoot >= forceGetProcRootUnsafe } type forceProcThreadSelfLevel int From 964931fa9d8bbf3d8f9fb8cbf9325e5d4d6d8c01 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 23 Jul 2024 15:14:09 +1000 Subject: [PATCH 2/5] tests: lookup: actually swap root in root-swap tests renameat2(fd, ".", ...) is not allowed, and so our rename-swap tests where we swap the root itself would silently do nothing (this explains why the racing tests would always succeed). The tests still pass, so our logic was correct, we just didn't exercise that particular check properly. Fixes: ac327434f02e ("tests: add racing tests for partialLokupInRoot and MkdirAllHandle") Signed-off-by: Aleksa Sarai --- lookup_linux_test.go | 14 ++++++++++++-- util_linux_test.go | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lookup_linux_test.go b/lookup_linux_test.go index f755366..56e7dfc 100644 --- a/lookup_linux_test.go +++ b/lookup_linux_test.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -458,8 +459,9 @@ func TestPartialLookup_RacingRename(t *testing.T) { "swap-dir-danglinglink-basic": {"a/b", "bad-link", "a/b/c/d/e", []error{unix.ENOENT}, slices.Clone(defaultExpected)}, "swap-dir-danglinglink-dotdot": {"a/b", "bad-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", []error{unix.ENOENT}, slices.Clone(defaultExpected)}, // Swap the root. - "swap-root-basic": {".", "../outsideroot", "a/b/c/d/e", nil, slices.Clone(defaultExpected)}, - "swap-root-dotdot": {".", "../outsideroot", "a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e", nil, slices.Clone(defaultExpected)}, + "swap-root-basic": {".", "../outsideroot", "a/b/c/d/e", nil, slices.Clone(defaultExpected)}, + "swap-root-dotdot": {".", "../outsideroot", "a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e", nil, slices.Clone(defaultExpected)}, + "swap-root-dotdot-extra": {".", "../outsideroot", "a/" + strings.Repeat("b/c/d/../../../", 10) + "b/c/d/e", nil, slices.Clone(defaultExpected)}, // Swap one of our walking paths outside the root. "swap-dir-outsideroot-basic": {"a/b", "../outsideroot", "a/b/c/d/e", nil, append( // We could hit the expected path. @@ -507,6 +509,14 @@ func TestPartialLookup_RacingRename(t *testing.T) { require.NoError(t, err) defer rootDir.Close() + // If the swapping subpaths are "." we need to use an absolute + // path because renaming "." isn't allowed. + for _, subPath := range []*string{&test.subPathA, &test.subPathB} { + if filepath.Join(root, *subPath) == root { + *subPath = root + } + } + // Run a goroutine that spams a rename in the root. pauseCh := make(chan struct{}) exitCh := make(chan struct{}) diff --git a/util_linux_test.go b/util_linux_test.go index 5f4c1d5..f4dcd2f 100644 --- a/util_linux_test.go +++ b/util_linux_test.go @@ -104,7 +104,12 @@ func doRenameExchangeLoop(pauseCh chan struct{}, exitCh <-chan struct{}, dir *os // Do the swap twice so that we only pause when we are in a // "correct" state. for i := 0; i < 2; i++ { - _ = unix.Renameat2(int(dir.Fd()), pathA, int(dir.Fd()), pathB, unix.RENAME_EXCHANGE) + err := unix.Renameat2(int(dir.Fd()), pathA, int(dir.Fd()), pathB, unix.RENAME_EXCHANGE) + if err != nil && !errors.Is(err, unix.EBADF) { + // Should never happen, and if it does we will potentially + // enter a bad filesystem state if we get paused. + panic(fmt.Sprintf("renameat2([%d]%q, %q, ..., %q, RENAME_EXCHANGE) = %v", int(dir.Fd()), dir.Name(), pathA, pathB, err)) + } } } } From 82c423ec588da0b351dab953ed21424e1b46ac47 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 23 Jul 2024 13:57:35 +1000 Subject: [PATCH 3/5] mkdir: fix *os.File leak when reopening starting path When switching away from O_PATH, we forgot to close the O_PATH handle when replacing it with the non-O_DIRECTORY handle. Fixes: ebb9f1f1dba0 ("mkdirall: switch away from O_PATH for mkdir loop") Signed-off-by: Aleksa Sarai --- mkdir_linux.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdir_linux.go b/mkdir_linux.go index 05e0bde..ed81172 100644 --- a/mkdir_linux.go +++ b/mkdir_linux.go @@ -82,6 +82,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err } else if err != nil { return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err) } else { + _ = currentDir.Close() currentDir = reopenDir } From b6bd99611be32be876dbc7c5c954e5ccbf37de69 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 23 Jul 2024 11:31:17 +1000 Subject: [PATCH 4/5] open: make OpenInRoot errors match a simple openat2 Because we use partialOpenInRoot as the backend implementation of OpenInRoot (which didn't return error information when a partial lookup succeeded), we would map all non-complete errors as ENOENT. This meant that for non-directories you didn't get ENOTDIR, which is what you'd get from a basic openat2(RESOLVE_IN_ROOT) using the path. While we could map the error in OpenInRoot to -ENOTDIR in simple cases, in dangling symlink cases OpenInRoot doesn't know what source error stopped the iteration. So we have to change the partialOpenInRoot API to return an error when a partial open is done, and all of the callers need to be updated to handle that. Since partialOpenInRoot is an internal API, this slightly unconventional interface (where a non-nil error is paired with actual value information) is not that bad. This also lets us remove a bit of duplication from partialOpenInRoot when handling a non-directory component, which I think makes things much nicer. Signed-off-by: Aleksa Sarai --- lookup_linux.go | 82 ++++++----------- lookup_linux_test.go | 212 ++++++++++++++++++++++--------------------- mkdir_linux.go | 6 +- open_linux.go | 13 ++- open_linux_test.go | 58 ++++++------ openat2_linux.go | 6 +- 6 files changed, 182 insertions(+), 195 deletions(-) diff --git a/lookup_linux.go b/lookup_linux.go index d242e97..072c9d7 100644 --- a/lookup_linux.go +++ b/lookup_linux.go @@ -157,7 +157,7 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { // within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing // component of the requested path, returning a file handle to the final // existing component and a string containing the remaining path components. -func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string, Err error) { +func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ string, _ error) { unsafePath = filepath.ToSlash(unsafePath) // noop // This is very similar to SecureJoin, except that we operate on the @@ -183,7 +183,8 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string return nil, "", fmt.Errorf("clone root fd: %w", err) } defer func() { - if Err != nil { + // If a handle is not returned, close the internal handle. + if Handle == nil { _ = currentDir.Close() } }() @@ -258,35 +259,6 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string } switch st.Mode() & os.ModeType { - case os.ModeDir: - // If we are dealing with a directory, simply walk into it. - _ = currentDir.Close() - currentDir = nextDir - currentPath = nextPath - - // The part was real, so drop it from the symlink stack. - if err := symlinkStack.PopPart(part); err != nil { - return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err) - } - - // If we are operating on a .., make sure we haven't escaped. - // We only have to check for ".." here because walking down - // into a regular component component cannot cause you to - // escape. This mirrors the logic in RESOLVE_IN_ROOT, except we - // have to check every ".." rather than only checking after a - // rename or mount on the system. - if part == ".." { - // Make sure the root hasn't moved. - if err := checkProcSelfFdPath(logicalRootPath, root); err != nil { - return nil, "", fmt.Errorf("root path moved during lookup: %w", err) - } - // Make sure the path is what we expect. - fullPath := logicalRootPath + nextPath - if err := checkProcSelfFdPath(fullPath, currentDir); err != nil { - return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err) - } - } - case os.ModeSymlink: // readlinkat implies AT_EMPTY_PATH. linkDest, err := readlinkatFile(nextDir, "") @@ -319,48 +291,50 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string currentDir = rootClone currentPath = "/" } + default: - // For any other file type, we can't walk further and so we've - // hit the end of the lookup. The handling is very similar to - // ENOENT from openat(2), except that we return a handle to the - // component we just walked into (and we drop the component - // from the symlink stack). + // If we are dealing with a directory, simply walk into it. _ = currentDir.Close() + currentDir = nextDir + currentPath = nextPath - // The part existed, so drop it from the symlink stack. + // The part was real, so drop it from the symlink stack. if err := symlinkStack.PopPart(part); err != nil { - return nil, "", fmt.Errorf("walking into non-directory %q failed: %w", part, err) + return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err) } - // If there are any remaining components in the symlink stack, - // we are still within a symlink resolution and thus we hit a - // dangling symlink. So pretend that the first symlink in the - // stack we hit was an ENOENT (to match openat2). - if oldDir, remainingPath, ok := symlinkStack.PopTopSymlink(); ok { - _ = nextDir.Close() - return oldDir, remainingPath, nil + // If we are operating on a .., make sure we haven't escaped. + // We only have to check for ".." here because walking down + // into a regular component component cannot cause you to + // escape. This mirrors the logic in RESOLVE_IN_ROOT, except we + // have to check every ".." rather than only checking after a + // rename or mount on the system. + if part == ".." { + // Make sure the root hasn't moved. + if err := checkProcSelfFdPath(logicalRootPath, root); err != nil { + return nil, "", fmt.Errorf("root path moved during lookup: %w", err) + } + // Make sure the path is what we expect. + fullPath := logicalRootPath + nextPath + if err := checkProcSelfFdPath(fullPath, currentDir); err != nil { + return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err) + } } - - // The current component exists, so return it. - return nextDir, remainingPath, nil } - case errors.Is(err, os.ErrNotExist): + default: // If there are any remaining components in the symlink stack, we // are still within a symlink resolution and thus we hit a dangling // symlink. So pretend that the first symlink in the stack we hit // was an ENOENT (to match openat2). if oldDir, remainingPath, ok := symlinkStack.PopTopSymlink(); ok { _ = currentDir.Close() - return oldDir, remainingPath, nil + return oldDir, remainingPath, err } // We have hit a final component that doesn't exist, so we have our // partial open result. Note that we have to use the OLD remaining // path, since the lookup failed. - return currentDir, oldRemainingPath, nil - - default: - return nil, "", err + return currentDir, oldRemainingPath, err } } // All of the components existed! diff --git a/lookup_linux_test.go b/lookup_linux_test.go index 56e7dfc..76e37ef 100644 --- a/lookup_linux_test.go +++ b/lookup_linux_test.go @@ -36,12 +36,20 @@ func checkPartialLookup(t *testing.T, partialLookupFn partialLookupFunc, rootDir if expected.err != nil { if assert.Error(t, err) { assert.ErrorIs(t, err, expected.err) - } else { - t.Errorf("unexpected handle %q", handle.Name()) } - return + if expected.handlePath == "" { + if handle != nil { + t.Errorf("unexpected handle %q", handle.Name()) + } + return + } + } else { + if expected.remainingPath != "" { + t.Errorf("we expect a remaining path, but no error? %q", expected.remainingPath) + } + assert.NoError(t, err) } - assert.NoError(t, err) + assert.NotNil(t, handle, "expected to get a handle") // Check the remainingPath. assert.Equal(t, expected.remainingPath, remainingPath, "remaining path") @@ -148,94 +156,94 @@ func testPartialLookup(t *testing.T, partialLookupFn partialLookupFunc) { "complete-fifo": {"b/fifo", lookupResult{handlePath: "/b/fifo", remainingPath: "", fileType: unix.S_IFIFO}}, "complete-sock": {"b/sock", lookupResult{handlePath: "/b/sock", remainingPath: "", fileType: unix.S_IFSOCK}}, // Partial lookups. - "partial-dir-basic": {"a/b/c/d/e/f/g/h", lookupResult{handlePath: "/a", remainingPath: "b/c/d/e/f/g/h", fileType: unix.S_IFDIR}}, - "partial-dir-dotdot": {"a/foo/../bar/baz", lookupResult{handlePath: "/a", remainingPath: "foo/../bar/baz", fileType: unix.S_IFDIR}}, + "partial-dir-basic": {"a/b/c/d/e/f/g/h", lookupResult{handlePath: "/a", remainingPath: "b/c/d/e/f/g/h", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "partial-dir-dotdot": {"a/foo/../bar/baz", lookupResult{handlePath: "/a", remainingPath: "foo/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, // Complete lookups of non-lexical symlinks. "nonlexical-basic-complete": {"target", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-basic-partial": {"target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-basic-partial-dotdot": {"target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-basic-partial": {"target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-basic-partial-dotdot": {"target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level1-abs-complete": {"link1/target_abs", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level1-abs-partial": {"link1/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level1-abs-partial-dotdot": {"link1/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level1-abs-partial": {"link1/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level1-abs-partial-dotdot": {"link1/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level1-rel-complete": {"link1/target_rel", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level1-rel-partial": {"link1/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level1-rel-partial-dotdot": {"link1/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level1-rel-partial": {"link1/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level1-rel-partial-dotdot": {"link1/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-abs-abs-complete": {"link2/link1_abs/target_abs", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-abs-partial": {"link2/link1_abs/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-abs-partial-dotdot": {"link2/link1_abs/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-abs-abs-partial": {"link2/link1_abs/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-abs-abs-partial-dotdot": {"link2/link1_abs/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-abs-rel-complete": {"link2/link1_abs/target_rel", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-rel-partial": {"link2/link1_abs/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-rel-partial-dotdot": {"link2/link1_abs/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-abs-rel-partial": {"link2/link1_abs/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-abs-rel-partial-dotdot": {"link2/link1_abs/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-abs-open-complete": {"link2/link1_abs/../target", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-open-partial": {"link2/link1_abs/../target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-abs-open-partial-dotdot": {"link2/link1_abs/../target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-abs-open-partial": {"link2/link1_abs/../target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-abs-open-partial-dotdot": {"link2/link1_abs/../target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-rel-abs-complete": {"link2/link1_rel/target_abs", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-abs-partial": {"link2/link1_rel/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-abs-partial-dotdot": {"link2/link1_rel/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-rel-abs-partial": {"link2/link1_rel/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-rel-abs-partial-dotdot": {"link2/link1_rel/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-rel-rel-complete": {"link2/link1_rel/target_rel", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-rel-partial": {"link2/link1_rel/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-rel-partial-dotdot": {"link2/link1_rel/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-rel-rel-partial": {"link2/link1_rel/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-rel-rel-partial-dotdot": {"link2/link1_rel/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level2-rel-open-complete": {"link2/link1_rel/../target", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-open-partial": {"link2/link1_rel/../target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level2-rel-open-partial-dotdot": {"link2/link1_rel/../target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level2-rel-open-partial": {"link2/link1_rel/../target/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level2-rel-open-partial-dotdot": {"link2/link1_rel/../target/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level3-abs-complete": {"link3/target_abs", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level3-abs-partial": {"link3/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level3-abs-partial-dotdot": {"link3/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level3-abs-partial": {"link3/target_abs/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level3-abs-partial-dotdot": {"link3/target_abs/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, "nonlexical-level3-rel-complete": {"link3/target_rel", lookupResult{handlePath: "/target", remainingPath: "", fileType: unix.S_IFDIR}}, - "nonlexical-level3-rel-partial": {"link3/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR}}, - "nonlexical-level3-rel-partial-dotdot": {"link3/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR}}, + "nonlexical-level3-rel-partial": {"link3/target_rel/foo", lookupResult{handlePath: "/target", remainingPath: "foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "nonlexical-level3-rel-partial-dotdot": {"link3/target_rel/../target/foo/bar/../baz", lookupResult{handlePath: "/target", remainingPath: "foo/bar/../baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, // Partial lookups due to hitting a non-directory. - "partial-nondir-dot": {"b/c/file/.", lookupResult{handlePath: "/b/c/file", remainingPath: ".", fileType: unix.S_IFREG}}, - "partial-nondir-dotdot1": {"b/c/file/..", lookupResult{handlePath: "/b/c/file", remainingPath: "..", fileType: unix.S_IFREG}}, - "partial-nondir-dotdot2": {"b/c/file/../foo/bar", lookupResult{handlePath: "/b/c/file", remainingPath: "../foo/bar", fileType: unix.S_IFREG}}, - "partial-nondir-symlink-dot": {"b-file/.", lookupResult{handlePath: "/b/c/file", remainingPath: ".", fileType: unix.S_IFREG}}, - "partial-nondir-symlink-dotdot1": {"b-file/..", lookupResult{handlePath: "/b/c/file", remainingPath: "..", fileType: unix.S_IFREG}}, - "partial-nondir-symlink-dotdot2": {"b-file/../foo/bar", lookupResult{handlePath: "/b/c/file", remainingPath: "../foo/bar", fileType: unix.S_IFREG}}, - "partial-fifo-dot": {"b/fifo/.", lookupResult{handlePath: "/b/fifo", remainingPath: ".", fileType: unix.S_IFIFO}}, - "partial-fifo-dotdot1": {"b/fifo/..", lookupResult{handlePath: "/b/fifo", remainingPath: "..", fileType: unix.S_IFIFO}}, - "partial-fifo-dotdot2": {"b/fifo/../foo/bar", lookupResult{handlePath: "/b/fifo", remainingPath: "../foo/bar", fileType: unix.S_IFIFO}}, - "partial-sock-dot": {"b/sock/.", lookupResult{handlePath: "/b/sock", remainingPath: ".", fileType: unix.S_IFSOCK}}, - "partial-sock-dotdot1": {"b/sock/..", lookupResult{handlePath: "/b/sock", remainingPath: "..", fileType: unix.S_IFSOCK}}, - "partial-sock-dotdot2": {"b/sock/../foo/bar", lookupResult{handlePath: "/b/sock", remainingPath: "../foo/bar", fileType: unix.S_IFSOCK}}, + "partial-nondir-dot": {"b/c/file/.", lookupResult{handlePath: "/b/c/file", remainingPath: ".", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-nondir-dotdot1": {"b/c/file/..", lookupResult{handlePath: "/b/c/file", remainingPath: "..", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-nondir-dotdot2": {"b/c/file/../foo/bar", lookupResult{handlePath: "/b/c/file", remainingPath: "../foo/bar", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-nondir-symlink-dot": {"b-file/.", lookupResult{handlePath: "/b/c/file", remainingPath: ".", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-nondir-symlink-dotdot1": {"b-file/..", lookupResult{handlePath: "/b/c/file", remainingPath: "..", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-nondir-symlink-dotdot2": {"b-file/../foo/bar", lookupResult{handlePath: "/b/c/file", remainingPath: "../foo/bar", fileType: unix.S_IFREG, err: unix.ENOTDIR}}, + "partial-fifo-dot": {"b/fifo/.", lookupResult{handlePath: "/b/fifo", remainingPath: ".", fileType: unix.S_IFIFO, err: unix.ENOTDIR}}, + "partial-fifo-dotdot1": {"b/fifo/..", lookupResult{handlePath: "/b/fifo", remainingPath: "..", fileType: unix.S_IFIFO, err: unix.ENOTDIR}}, + "partial-fifo-dotdot2": {"b/fifo/../foo/bar", lookupResult{handlePath: "/b/fifo", remainingPath: "../foo/bar", fileType: unix.S_IFIFO, err: unix.ENOTDIR}}, + "partial-sock-dot": {"b/sock/.", lookupResult{handlePath: "/b/sock", remainingPath: ".", fileType: unix.S_IFSOCK, err: unix.ENOTDIR}}, + "partial-sock-dotdot1": {"b/sock/..", lookupResult{handlePath: "/b/sock", remainingPath: "..", fileType: unix.S_IFSOCK, err: unix.ENOTDIR}}, + "partial-sock-dotdot2": {"b/sock/../foo/bar", lookupResult{handlePath: "/b/sock", remainingPath: "../foo/bar", fileType: unix.S_IFSOCK, err: unix.ENOTDIR}}, // Dangling symlinks are treated as though they are non-existent. - "dangling1-inroot-trailing": {"a-fake1", lookupResult{handlePath: "/", remainingPath: "a-fake1", fileType: unix.S_IFDIR}}, - "dangling1-inroot-partial": {"a-fake1/foo", lookupResult{handlePath: "/", remainingPath: "a-fake1/foo", fileType: unix.S_IFDIR}}, - "dangling1-inroot-partial-dotdot": {"a-fake1/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake1/../bar/baz", fileType: unix.S_IFDIR}}, - "dangling1-sub-trailing": {"c/a-fake1", lookupResult{handlePath: "/c", remainingPath: "a-fake1", fileType: unix.S_IFDIR}}, - "dangling1-sub-partial": {"c/a-fake1/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake1/foo", fileType: unix.S_IFDIR}}, - "dangling1-sub-partial-dotdot": {"c/a-fake1/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake1/../bar/baz", fileType: unix.S_IFDIR}}, - "dangling2-inroot-trailing": {"a-fake2", lookupResult{handlePath: "/", remainingPath: "a-fake2", fileType: unix.S_IFDIR}}, - "dangling2-inroot-partial": {"a-fake2/foo", lookupResult{handlePath: "/", remainingPath: "a-fake2/foo", fileType: unix.S_IFDIR}}, - "dangling2-inroot-partial-dotdot": {"a-fake2/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake2/../bar/baz", fileType: unix.S_IFDIR}}, - "dangling2-sub-trailing": {"c/a-fake2", lookupResult{handlePath: "/c", remainingPath: "a-fake2", fileType: unix.S_IFDIR}}, - "dangling2-sub-partial": {"c/a-fake2/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake2/foo", fileType: unix.S_IFDIR}}, - "dangling2-sub-partial-dotdot": {"c/a-fake2/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake2/../bar/baz", fileType: unix.S_IFDIR}}, - "dangling3-inroot-trailing": {"a-fake3", lookupResult{handlePath: "/", remainingPath: "a-fake3", fileType: unix.S_IFDIR}}, - "dangling3-inroot-partial": {"a-fake3/foo", lookupResult{handlePath: "/", remainingPath: "a-fake3/foo", fileType: unix.S_IFDIR}}, - "dangling3-inroot-partial-dotdot": {"a-fake3/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake3/../bar/baz", fileType: unix.S_IFDIR}}, - "dangling3-sub-trailing": {"c/a-fake3", lookupResult{handlePath: "/c", remainingPath: "a-fake3", fileType: unix.S_IFDIR}}, - "dangling3-sub-partial": {"c/a-fake3/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake3/foo", fileType: unix.S_IFDIR}}, - "dangling3-sub-partial-dotdot": {"c/a-fake3/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake3/../bar/baz", fileType: unix.S_IFDIR}}, + "dangling1-inroot-trailing": {"a-fake1", lookupResult{handlePath: "/", remainingPath: "a-fake1", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling1-inroot-partial": {"a-fake1/foo", lookupResult{handlePath: "/", remainingPath: "a-fake1/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling1-inroot-partial-dotdot": {"a-fake1/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake1/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling1-sub-trailing": {"c/a-fake1", lookupResult{handlePath: "/c", remainingPath: "a-fake1", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling1-sub-partial": {"c/a-fake1/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake1/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling1-sub-partial-dotdot": {"c/a-fake1/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake1/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-inroot-trailing": {"a-fake2", lookupResult{handlePath: "/", remainingPath: "a-fake2", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-inroot-partial": {"a-fake2/foo", lookupResult{handlePath: "/", remainingPath: "a-fake2/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-inroot-partial-dotdot": {"a-fake2/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake2/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-sub-trailing": {"c/a-fake2", lookupResult{handlePath: "/c", remainingPath: "a-fake2", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-sub-partial": {"c/a-fake2/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake2/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling2-sub-partial-dotdot": {"c/a-fake2/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake2/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-inroot-trailing": {"a-fake3", lookupResult{handlePath: "/", remainingPath: "a-fake3", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-inroot-partial": {"a-fake3/foo", lookupResult{handlePath: "/", remainingPath: "a-fake3/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-inroot-partial-dotdot": {"a-fake3/../bar/baz", lookupResult{handlePath: "/", remainingPath: "a-fake3/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-sub-trailing": {"c/a-fake3", lookupResult{handlePath: "/c", remainingPath: "a-fake3", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-sub-partial": {"c/a-fake3/foo", lookupResult{handlePath: "/c", remainingPath: "a-fake3/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling3-sub-partial-dotdot": {"c/a-fake3/../bar/baz", lookupResult{handlePath: "/c", remainingPath: "a-fake3/../bar/baz", fileType: unix.S_IFDIR, err: unix.ENOENT}}, // Tricky dangling symlinks. - "dangling-tricky1-trailing": {"link3/deep_dangling1", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1", fileType: unix.S_IFDIR}}, - "dangling-tricky1-partial": {"link3/deep_dangling1/foo", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1/foo", fileType: unix.S_IFDIR}}, - "dangling-tricky1-partial-dotdot": {"link3/deep_dangling1/..", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1/..", fileType: unix.S_IFDIR}}, - "dangling-tricky2-trailing": {"link3/deep_dangling2", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2", fileType: unix.S_IFDIR}}, - "dangling-tricky2-partial": {"link3/deep_dangling2/foo", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2/foo", fileType: unix.S_IFDIR}}, - "dangling-tricky2-partial-dotdot": {"link3/deep_dangling2/..", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2/..", fileType: unix.S_IFDIR}}, + "dangling-tricky1-trailing": {"link3/deep_dangling1", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling-tricky1-partial": {"link3/deep_dangling1/foo", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling-tricky1-partial-dotdot": {"link3/deep_dangling1/..", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling1/..", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling-tricky2-trailing": {"link3/deep_dangling2", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling-tricky2-partial": {"link3/deep_dangling2/foo", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2/foo", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "dangling-tricky2-partial-dotdot": {"link3/deep_dangling2/..", lookupResult{handlePath: "/link3", remainingPath: "deep_dangling2/..", fileType: unix.S_IFDIR, err: unix.ENOENT}}, // Really deep dangling links. - "deep-dangling1": {"dangling/a", lookupResult{handlePath: "/dangling", remainingPath: "a", fileType: unix.S_IFDIR}}, - "deep-dangling2": {"dangling/b/c", lookupResult{handlePath: "/dangling/b", remainingPath: "c", fileType: unix.S_IFDIR}}, - "deep-dangling3": {"dangling/c", lookupResult{handlePath: "/dangling", remainingPath: "c", fileType: unix.S_IFDIR}}, - "deep-dangling4": {"dangling/d/e", lookupResult{handlePath: "/dangling/d", remainingPath: "e", fileType: unix.S_IFDIR}}, - "deep-dangling5": {"dangling/e", lookupResult{handlePath: "/dangling", remainingPath: "e", fileType: unix.S_IFDIR}}, - "deep-dangling6": {"dangling/g", lookupResult{handlePath: "/dangling", remainingPath: "g", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir1": {"dangling-file/a", lookupResult{handlePath: "/dangling-file", remainingPath: "a", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir2": {"dangling-file/b/c", lookupResult{handlePath: "/dangling-file/b", remainingPath: "c", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir3": {"dangling-file/c", lookupResult{handlePath: "/dangling-file", remainingPath: "c", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir4": {"dangling-file/d/e", lookupResult{handlePath: "/dangling-file/d", remainingPath: "e", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir5": {"dangling-file/e", lookupResult{handlePath: "/dangling-file", remainingPath: "e", fileType: unix.S_IFDIR}}, - "deep-dangling-fileasdir6": {"dangling-file/g", lookupResult{handlePath: "/dangling-file", remainingPath: "g", fileType: unix.S_IFDIR}}, + "deep-dangling1": {"dangling/a", lookupResult{handlePath: "/dangling", remainingPath: "a", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling2": {"dangling/b/c", lookupResult{handlePath: "/dangling/b", remainingPath: "c", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling3": {"dangling/c", lookupResult{handlePath: "/dangling", remainingPath: "c", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling4": {"dangling/d/e", lookupResult{handlePath: "/dangling/d", remainingPath: "e", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling5": {"dangling/e", lookupResult{handlePath: "/dangling", remainingPath: "e", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling6": {"dangling/g", lookupResult{handlePath: "/dangling", remainingPath: "g", fileType: unix.S_IFDIR, err: unix.ENOENT}}, + "deep-dangling-fileasdir1": {"dangling-file/a", lookupResult{handlePath: "/dangling-file", remainingPath: "a", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, + "deep-dangling-fileasdir2": {"dangling-file/b/c", lookupResult{handlePath: "/dangling-file/b", remainingPath: "c", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, + "deep-dangling-fileasdir3": {"dangling-file/c", lookupResult{handlePath: "/dangling-file", remainingPath: "c", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, + "deep-dangling-fileasdir4": {"dangling-file/d/e", lookupResult{handlePath: "/dangling-file/d", remainingPath: "e", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, + "deep-dangling-fileasdir5": {"dangling-file/e", lookupResult{handlePath: "/dangling-file", remainingPath: "e", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, + "deep-dangling-fileasdir6": {"dangling-file/g", lookupResult{handlePath: "/dangling-file", remainingPath: "g", fileType: unix.S_IFDIR, err: unix.ENOTDIR}}, // Symlink loops. "loop": {"loop/link", lookupResult{err: unix.ELOOP}}, "loop-basic1": {"loop/basic-loop1", lookupResult{err: unix.ELOOP}}, @@ -290,12 +298,12 @@ func TestPartialLookupInRoot_BadInode(t *testing.T) { "char-trailing": {"foo/whiteout", lookupResult{handlePath: "/foo/whiteout", remainingPath: "", fileType: unix.S_IFCHR}}, "blk-trailing": {"foo/whiteout-blk", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: "", fileType: unix.S_IFBLK}}, // Partial lookups due to hitting a non-directory. - "char-dot": {"foo/whiteout/.", lookupResult{handlePath: "/foo/whiteout", remainingPath: ".", fileType: unix.S_IFCHR}}, - "char-dotdot1": {"foo/whiteout/..", lookupResult{handlePath: "/foo/whiteout", remainingPath: "..", fileType: unix.S_IFCHR}}, - "char-dotdot2": {"foo/whiteout/../foo/bar", lookupResult{handlePath: "/foo/whiteout", remainingPath: "../foo/bar", fileType: unix.S_IFCHR}}, - "blk-dot": {"foo/whiteout-blk/.", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: ".", fileType: unix.S_IFBLK}}, - "blk-dotdot1": {"foo/whiteout-blk/..", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: "..", fileType: unix.S_IFBLK}}, - "blk-dotdot2": {"foo/whiteout-blk/../foo/bar", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: "../foo/bar", fileType: unix.S_IFBLK}}, + "char-dot": {"foo/whiteout/.", lookupResult{handlePath: "/foo/whiteout", remainingPath: ".", fileType: unix.S_IFCHR, err: unix.ENOTDIR}}, + "char-dotdot1": {"foo/whiteout/..", lookupResult{handlePath: "/foo/whiteout", remainingPath: "..", fileType: unix.S_IFCHR, err: unix.ENOTDIR}}, + "char-dotdot2": {"foo/whiteout/../foo/bar", lookupResult{handlePath: "/foo/whiteout", remainingPath: "../foo/bar", fileType: unix.S_IFCHR, err: unix.ENOTDIR}}, + "blk-dot": {"foo/whiteout-blk/.", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: ".", fileType: unix.S_IFBLK, err: unix.ENOTDIR}}, + "blk-dotdot1": {"foo/whiteout-blk/..", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: "..", fileType: unix.S_IFBLK, err: unix.ENOTDIR}}, + "blk-dotdot2": {"foo/whiteout-blk/../foo/bar", lookupResult{handlePath: "/foo/whiteout-blk", remainingPath: "../foo/bar", fileType: unix.S_IFBLK, err: unix.ENOTDIR}}, } { test := test // copy iterator // Update the handlePath to be inside our root. @@ -327,27 +335,6 @@ func (m *racingLookupMeta) checkPartialLookup(t *testing.T, rootDir *os.File, un // Similar to checkPartialLookup, but with extra logic for // handling the lookup stopping partly through the lookup. handle, remainingPath, err := partialLookupInRoot(rootDir, unsafePath) - if err != nil { - for _, skipErr := range skipErrs { - if errors.Is(err, skipErr) { - m.skipErrCounts[skipErr]++ - m.skipCount++ - return - } - } - for _, allowed := range allowedResults { - if allowed.err != nil && errors.Is(err, allowed.err) { - m.passErrCount++ - return - } - } - // If we didn't hit any of the allowed errors, it's an - // unexpected error. - assert.NoError(t, err) - m.badErrCount++ - return - } - var ( handleName string realPath string @@ -366,6 +353,25 @@ func (m *racingLookupMeta) checkPartialLookup(t *testing.T, rootDir *os.File, un require.NoError(t, err, "stat handle") _ = handle.Close() + } else if err != nil { + for _, skipErr := range skipErrs { + if errors.Is(err, skipErr) { + m.skipErrCounts[skipErr]++ + m.skipCount++ + return + } + } + for _, allowed := range allowedResults { + if allowed.err != nil && errors.Is(err, allowed.err) { + m.passErrCount++ + return + } + } + // If we didn't hit any of the allowed errors, it's an + // unexpected error. + assert.NoError(t, err) + m.badErrCount++ + return } if realPath != handleName { diff --git a/mkdir_linux.go b/mkdir_linux.go index ed81172..ad2bd79 100644 --- a/mkdir_linux.go +++ b/mkdir_linux.go @@ -49,14 +49,14 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err // Try to open as much of the path as possible. currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath) - if err != nil { - return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err) - } defer func() { if Err != nil { _ = currentDir.Close() } }() + if err != nil && !errors.Is(err, unix.ENOENT) { + return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err) + } // If there is an attacker deleting directories as we walk into them, // detect this proactively. Note this is guaranteed to detect if the diff --git a/open_linux.go b/open_linux.go index 2170061..d50ac42 100644 --- a/open_linux.go +++ b/open_linux.go @@ -17,12 +17,15 @@ import ( // using an *os.File handle, to ensure that the correct root directory is used. func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { handle, remainingPath, err := partialLookupInRoot(root, unsafePath) - if err != nil { - return nil, err + if remainingPath != "" && err == nil { + // This should never happen. + err = unix.ENOENT } - if remainingPath != "" { - _ = handle.Close() - return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: unix.ENOENT} + if err != nil { + if handle != nil { + _ = handle.Close() + } + return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err} } return handle, nil } diff --git a/open_linux_test.go b/open_linux_test.go index eb794c0..6cd2e36 100644 --- a/open_linux_test.go +++ b/open_linux_test.go @@ -209,10 +209,12 @@ func testOpenInRoot(t *testing.T, openInRootFn openInRootFunc) { expected openResult }{ // Complete lookups. - "complete-dir1": {"a", openResult{handlePath: "/a", fileType: unix.S_IFDIR}}, - "complete-dir2": {"b/c/d/e/f", openResult{handlePath: "/b/c/d/e/f", fileType: unix.S_IFDIR}}, - "complete-fifo": {"b/fifo", openResult{handlePath: "/b/fifo", fileType: unix.S_IFIFO}}, - "complete-sock": {"b/sock", openResult{handlePath: "/b/sock", fileType: unix.S_IFSOCK}}, + "complete-dir1": {"a", openResult{handlePath: "/a", fileType: unix.S_IFDIR}}, + "complete-dir2": {"b/c/d/e/f", openResult{handlePath: "/b/c/d/e/f", fileType: unix.S_IFDIR}}, + "complete-file": {"b/c/file", openResult{handlePath: "/b/c/file", fileType: unix.S_IFREG}}, + "complete-file-link": {"b-file", openResult{handlePath: "/b/c/file", fileType: unix.S_IFREG}}, + "complete-fifo": {"b/fifo", openResult{handlePath: "/b/fifo", fileType: unix.S_IFIFO}}, + "complete-sock": {"b/sock", openResult{handlePath: "/b/sock", fileType: unix.S_IFSOCK}}, // Partial lookups. "partial-dir-basic": {"a/b/c/d/e/f/g/h", openResult{err: unix.ENOENT}}, "partial-dir-dotdot": {"a/foo/../bar/baz", openResult{err: unix.ENOENT}}, @@ -251,18 +253,18 @@ func testOpenInRoot(t *testing.T, openInRootFn openInRootFunc) { "nonlexical-level3-rel-partial": {"link3/target_rel/foo", openResult{err: unix.ENOENT}}, "nonlexical-level3-rel-partial-dotdot": {"link3/target_rel/../target/foo/bar/../baz", openResult{err: unix.ENOENT}}, // Partial lookups due to hitting a non-directory. - "partial-nondir-dot": {"b/c/file/.", openResult{err: unix.ENOENT}}, - "partial-nondir-dotdot1": {"b/c/file/..", openResult{err: unix.ENOENT}}, - "partial-nondir-dotdot2": {"b/c/file/../foo/bar", openResult{err: unix.ENOENT}}, - "partial-nondir-symlink-dot": {"b-file/.", openResult{err: unix.ENOENT}}, - "partial-nondir-symlink-dotdot1": {"b-file/..", openResult{err: unix.ENOENT}}, - "partial-nondir-symlink-dotdot2": {"b-file/../foo/bar", openResult{err: unix.ENOENT}}, - "partial-fifo-dot": {"b/fifo/.", openResult{err: unix.ENOENT}}, - "partial-fifo-dotdot1": {"b/fifo/..", openResult{err: unix.ENOENT}}, - "partial-fifo-dotdot2": {"b/fifo/../foo/bar", openResult{err: unix.ENOENT}}, - "partial-sock-dot": {"b/sock/.", openResult{err: unix.ENOENT}}, - "partial-sock-dotdot1": {"b/sock/..", openResult{err: unix.ENOENT}}, - "partial-sock-dotdot2": {"b/sock/../foo/bar", openResult{err: unix.ENOENT}}, + "partial-nondir-dot": {"b/c/file/.", openResult{err: unix.ENOTDIR}}, + "partial-nondir-dotdot1": {"b/c/file/..", openResult{err: unix.ENOTDIR}}, + "partial-nondir-dotdot2": {"b/c/file/../foo/bar", openResult{err: unix.ENOTDIR}}, + "partial-nondir-symlink-dot": {"b-file/.", openResult{err: unix.ENOTDIR}}, + "partial-nondir-symlink-dotdot1": {"b-file/..", openResult{err: unix.ENOTDIR}}, + "partial-nondir-symlink-dotdot2": {"b-file/../foo/bar", openResult{err: unix.ENOTDIR}}, + "partial-fifo-dot": {"b/fifo/.", openResult{err: unix.ENOTDIR}}, + "partial-fifo-dotdot1": {"b/fifo/..", openResult{err: unix.ENOTDIR}}, + "partial-fifo-dotdot2": {"b/fifo/../foo/bar", openResult{err: unix.ENOTDIR}}, + "partial-sock-dot": {"b/sock/.", openResult{err: unix.ENOTDIR}}, + "partial-sock-dotdot1": {"b/sock/..", openResult{err: unix.ENOTDIR}}, + "partial-sock-dotdot2": {"b/sock/../foo/bar", openResult{err: unix.ENOTDIR}}, // Dangling symlinks are treated as though they are non-existent. "dangling1-inroot-trailing": {"a-fake1", openResult{err: unix.ENOENT}}, "dangling1-inroot-partial": {"a-fake1/foo", openResult{err: unix.ENOENT}}, @@ -296,12 +298,12 @@ func testOpenInRoot(t *testing.T, openInRootFn openInRootFunc) { "deep-dangling4": {"dangling/d/e", openResult{err: unix.ENOENT}}, "deep-dangling5": {"dangling/e", openResult{err: unix.ENOENT}}, "deep-dangling6": {"dangling/g", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir1": {"dangling-file/a", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir2": {"dangling-file/b/c", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir3": {"dangling-file/c", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir4": {"dangling-file/d/e", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir5": {"dangling-file/e", openResult{err: unix.ENOENT}}, - "deep-dangling-fileasdir6": {"dangling-file/g", openResult{err: unix.ENOENT}}, + "deep-dangling-fileasdir1": {"dangling-file/a", openResult{err: unix.ENOTDIR}}, + "deep-dangling-fileasdir2": {"dangling-file/b/c", openResult{err: unix.ENOTDIR}}, + "deep-dangling-fileasdir3": {"dangling-file/c", openResult{err: unix.ENOTDIR}}, + "deep-dangling-fileasdir4": {"dangling-file/d/e", openResult{err: unix.ENOTDIR}}, + "deep-dangling-fileasdir5": {"dangling-file/e", openResult{err: unix.ENOTDIR}}, + "deep-dangling-fileasdir6": {"dangling-file/g", openResult{err: unix.ENOTDIR}}, // Symlink loops. "loop": {"loop/link", openResult{err: unix.ELOOP}}, "loop-basic1": {"loop/basic-loop1", openResult{err: unix.ELOOP}}, @@ -364,12 +366,12 @@ func TestOpenInRoot_BadInode(t *testing.T) { "char-trailing": {"foo/whiteout", openResult{handlePath: "/foo/whiteout", fileType: unix.S_IFCHR}}, "blk-trailing": {"foo/whiteout-blk", openResult{handlePath: "/foo/whiteout-blk", fileType: unix.S_IFBLK}}, // Partial lookups due to hitting a non-directory. - "char-dot": {"foo/whiteout/.", openResult{err: unix.ENOENT}}, - "char-dotdot1": {"foo/whiteout/..", openResult{err: unix.ENOENT}}, - "char-dotdot2": {"foo/whiteout/../foo/bar", openResult{err: unix.ENOENT}}, - "blk-dot": {"foo/whiteout-blk/.", openResult{err: unix.ENOENT}}, - "blk-dotdot1": {"foo/whiteout-blk/..", openResult{err: unix.ENOENT}}, - "blk-dotdot2": {"foo/whiteout-blk/../foo/bar", openResult{err: unix.ENOENT}}, + "char-dot": {"foo/whiteout/.", openResult{err: unix.ENOTDIR}}, + "char-dotdot1": {"foo/whiteout/..", openResult{err: unix.ENOTDIR}}, + "char-dotdot2": {"foo/whiteout/../foo/bar", openResult{err: unix.ENOTDIR}}, + "blk-dot": {"foo/whiteout-blk/.", openResult{err: unix.ENOTDIR}}, + "blk-dotdot1": {"foo/whiteout-blk/..", openResult{err: unix.ENOTDIR}}, + "blk-dotdot2": {"foo/whiteout-blk/../foo/bar", openResult{err: unix.ENOTDIR}}, } { test := test // copy iterator // Update the handlePath to be inside our root. diff --git a/openat2_linux.go b/openat2_linux.go index fc93db8..b4b95dc 100644 --- a/openat2_linux.go +++ b/openat2_linux.go @@ -95,6 +95,7 @@ func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, e unsafePath = filepath.ToSlash(unsafePath) // noop endIdx := len(unsafePath) + var lastError error for endIdx > 0 { subpath := unsafePath[:endIdx] @@ -108,11 +109,12 @@ func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, e endIdx += 1 } // We found a subpath! - return handle, unsafePath[endIdx:], nil + return handle, unsafePath[endIdx:], lastError } if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) { // That path doesn't exist, let's try the next directory up. endIdx = strings.LastIndexByte(subpath, '/') + lastError = err continue } return nil, "", fmt.Errorf("open subpath: %w", err) @@ -124,5 +126,5 @@ func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, e if err != nil { return nil, "", err } - return rootClone, unsafePath, nil + return rootClone, unsafePath, lastError } From 1f4688ac6c359fd4fbf2d4197f2b4b3ba3a9b756 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 23 Jul 2024 14:21:29 +1000 Subject: [PATCH 5/5] lookup: special-case non-partial lookups For openat2 this means we can just one-shot the lookup (making our lookups faster) and for partialLookupInRoot we can not bother with the symlink stack (and simplify the error handling case when doing a complete lookup). Signed-off-by: Aleksa Sarai --- lookup_linux.go | 60 ++++++++++++++++++++++++++++++++++-------------- open_linux.go | 9 +------- openat2_linux.go | 11 +++++++++ 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/lookup_linux.go b/lookup_linux.go index 072c9d7..da8efad 100644 --- a/lookup_linux.go +++ b/lookup_linux.go @@ -40,16 +40,18 @@ func (se symlinkStackEntry) Close() { type symlinkStack []*symlinkStackEntry -func (s symlinkStack) IsEmpty() bool { - return len(s) == 0 +func (s *symlinkStack) IsEmpty() bool { + return s == nil || len(*s) == 0 } func (s *symlinkStack) Close() { - for _, link := range *s { - link.Close() + if s != nil { + for _, link := range *s { + link.Close() + } + // TODO: Switch to clear once we switch to Go 1.21. + *s = nil } - // TODO: Switch to clear once we switch to Go 1.21. - *s = nil } var ( @@ -58,7 +60,7 @@ var ( ) func (s *symlinkStack) popPart(part string) error { - if s.IsEmpty() { + if s == nil || s.IsEmpty() { // If there is nothing in the symlink stack, then the part was from the // real path provided by the user, and this is a no-op. return errEmptyStack @@ -102,6 +104,9 @@ func (s *symlinkStack) PopPart(part string) error { } func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error { + if s == nil { + return nil + } // Split the link target and clean up any "" parts. linkTargetParts := slices.DeleteFunc( strings.Split(linkTarget, "/"), @@ -145,7 +150,7 @@ func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, li } func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { - if s.IsEmpty() { + if s == nil || s.IsEmpty() { return nil, "", false } tailEntry := (*s)[0] @@ -157,7 +162,22 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { // within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing // component of the requested path, returning a file handle to the final // existing component and a string containing the remaining path components. -func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ string, _ error) { +func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) { + return lookupInRoot(root, unsafePath, true) +} + +func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) { + handle, remainingPath, err := lookupInRoot(root, unsafePath, false) + if remainingPath != "" && err == nil { + // should never happen + err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath) + } + // lookupInRoot(partial=false) will always close the handle if an error is + // returned, so no need to double-check here. + return handle, err +} + +func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) { unsafePath = filepath.ToSlash(unsafePath) // noop // This is very similar to SecureJoin, except that we operate on the @@ -166,7 +186,7 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s // Try to use openat2 if possible. if hasOpenat2() { - return partialLookupOpenat2(root, unsafePath) + return lookupOpenat2(root, unsafePath, partial) } // Get the "actual" root path from /proc/self/fd. This is necessary if the @@ -201,8 +221,11 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s // Note that the stack is ONLY used for book-keeping. All of the actual // path walking logic is still based on currentPath/remainingPath and // currentDir (as in SecureJoin). - var symlinkStack symlinkStack - defer symlinkStack.Close() + var symStack *symlinkStack + if partial { + symStack = new(symlinkStack) + defer symStack.Close() + } var ( linksWalked int @@ -234,7 +257,7 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s // If we logically hit the root, just clone the root rather than // opening the part and doing all of the other checks. if nextPath == "/" { - if err := symlinkStack.PopPart(part); err != nil { + if err := symStack.PopPart(part); err != nil { return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err) } // Jump to root. @@ -270,11 +293,11 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s linksWalked++ if linksWalked > maxSymlinkLimit { - return nil, "", &os.PathError{Op: "partialLookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP} + return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP} } // Swap out the symlink's component for the link entry itself. - if err := symlinkStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil { + if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil { return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err) } @@ -299,7 +322,7 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s currentPath = nextPath // The part was real, so drop it from the symlink stack. - if err := symlinkStack.PopPart(part); err != nil { + if err := symStack.PopPart(part); err != nil { return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err) } @@ -323,11 +346,14 @@ func partialLookupInRoot(root *os.File, unsafePath string) (Handle *os.File, _ s } default: + if !partial { + return nil, "", err + } // If there are any remaining components in the symlink stack, we // are still within a symlink resolution and thus we hit a dangling // symlink. So pretend that the first symlink in the stack we hit // was an ENOENT (to match openat2). - if oldDir, remainingPath, ok := symlinkStack.PopTopSymlink(); ok { + if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok { _ = currentDir.Close() return oldDir, remainingPath, err } diff --git a/open_linux.go b/open_linux.go index d50ac42..39ea1ba 100644 --- a/open_linux.go +++ b/open_linux.go @@ -16,15 +16,8 @@ import ( // OpenatInRoot is equivalent to OpenInRoot, except that the root is provided // using an *os.File handle, to ensure that the correct root directory is used. func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { - handle, remainingPath, err := partialLookupInRoot(root, unsafePath) - if remainingPath != "" && err == nil { - // This should never happen. - err = unix.ENOENT - } + handle, err := completeLookupInRoot(root, unsafePath) if err != nil { - if handle != nil { - _ = handle.Close() - } return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err} } return handle, nil diff --git a/openat2_linux.go b/openat2_linux.go index b4b95dc..921b3e1 100644 --- a/openat2_linux.go +++ b/openat2_linux.go @@ -87,6 +87,17 @@ func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack} } +func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) { + if !partial { + file, err := openat2File(root, unsafePath, &unix.OpenHow{ + Flags: unix.O_PATH | unix.O_CLOEXEC, + Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, + }) + return file, "", err + } + return partialLookupOpenat2(root, unsafePath) +} + // partialLookupOpenat2 is an alternative implementation of // partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a // handle to the deepest existing child of the requested path within the root.