src/os/root.go | 15 ++++++++------- src/os/root_js.go | 23 ++++++++++++++++++----- src/os/root_openat.go | 17 ++++++++++++++--- src/os/root_test.go | 34 ++++++++++++++++++++++++++++++++++ diff --git a/src/os/root.go b/src/os/root.go index f91c0f75f30e2aab0b2c31c0a0f206e07d922cd8..739410d96de0e94e7e0e0971ef167b2538aa9e56 100644 --- a/src/os/root.go +++ b/src/os/root.go @@ -186,20 +186,20 @@ // absolute, volume-relative, or "". // // "." components are removed, except in the last component. // -// Path separators following the last component are preserved. -func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error) { +// Path separators following the last component are returned in suffixSep. +func splitPathInRoot(s string, prefix, suffix []string) (_ []string, suffixSep string, err error) { if len(s) == 0 { - return nil, errors.New("empty path") + return nil, "", errors.New("empty path") } if IsPathSeparator(s[0]) { - return nil, errPathEscapes + return nil, "", errPathEscapes } if runtime.GOOS == "windows" { // Windows cleans paths before opening them. s, err = rootCleanPath(s, prefix, suffix) if err != nil { - return nil, err + return nil, "", err } prefix = nil suffix = nil @@ -215,13 +215,14 @@ continue } parts = append(parts, s[i:j]) // Advance to the next component, or end of the path. + partEnd := j for j < len(s) && IsPathSeparator(s[j]) { j++ } if j == len(s) { // If this is the last path component, // preserve any trailing path separators. - parts[len(parts)-1] = s[i:] + suffixSep = s[partEnd:] break } if parts[len(parts)-1] == "." { @@ -235,7 +236,7 @@ // Remove a trailing "." component if we're joining to a suffix. parts = parts[:len(parts)-1] } parts = append(parts, suffix...) - return parts, nil + return parts, suffixSep, nil } // FS returns a file system (an fs.FS) for the tree of files in the root. diff --git a/src/os/root_js.go b/src/os/root_js.go index 70aa5f9ccd0cd01479c20615866a52c26075ee3d..56a37dafe1626e5125c69f4d8c7bf9310bd8ffbe 100644 --- a/src/os/root_js.go +++ b/src/os/root_js.go @@ -33,7 +33,7 @@ func checkPathEscapesInternal(r *Root, name string, lstat bool) error { if r.root.closed.Load() { return ErrClosed } - parts, err := splitPathInRoot(name, nil, nil) + parts, suffixSep, err := splitPathInRoot(name, nil, nil) if err != nil { return err } @@ -61,11 +61,15 @@ } continue } - if lstat && i == len(parts)-1 { - break + part := parts[i] + if i == len(parts)-1 { + if lstat { + break + } + part += suffixSep } - next := joinPath(base, parts[i]) + next := joinPath(base, part) fi, err := Lstat(next) if err != nil { if IsNotExist(err) { @@ -82,9 +86,18 @@ symlinks++ if symlinks > rootMaxSymlinks { return errors.New("too many symlinks") } - newparts, err := splitPathInRoot(link, parts[:i], parts[i+1:]) + newparts, newSuffixSep, err := splitPathInRoot(link, parts[:i], parts[i+1:]) if err != nil { return err + } + if i == len(parts) { + // suffixSep contains any trailing path separator characters + // in the link target. + // If we are replacing the remainder of the path, retain these. + // If we're replacing some intermediate component of the path, + // ignore them, since intermediate components must always be + // directories. + suffixSep = newSuffixSep } parts = newparts continue diff --git a/src/os/root_openat.go b/src/os/root_openat.go index 6fc02a1a0718cf020001a5aad6a55f2b00c1dd4b..c974ce2c61d388e44dcdb6c02363820449c30eae 100644 --- a/src/os/root_openat.go +++ b/src/os/root_openat.go @@ -98,7 +98,7 @@ return ret, err } defer r.root.decref() - parts, err := splitPathInRoot(name, nil, nil) + parts, suffixSep, err := splitPathInRoot(name, nil, nil) if err != nil { return ret, err } @@ -162,7 +162,9 @@ // This is the last path element. // Call f to decide what to do with it. // If f returns errSymlink, this element is a symlink // which should be followed. - ret, err = f(dirfd, parts[i]) + // suffixSep contains any trailing separator characters + // which we rejoin to the final part at this time. + ret, err = f(dirfd, parts[i]+suffixSep) if _, ok := err.(errSymlink); !ok { return ret, err } @@ -184,9 +186,18 @@ symlinks++ if symlinks > rootMaxSymlinks { return ret, syscall.ELOOP } - newparts, err := splitPathInRoot(string(e), parts[:i], parts[i+1:]) + newparts, newSuffixSep, err := splitPathInRoot(string(e), parts[:i], parts[i+1:]) if err != nil { return ret, err + } + if i == len(parts)-1 { + // suffixSep contains any trailing path separator characters + // in the link target. + // If we are replacing the remainder of the path, retain these. + // If we're replacing some intermediate component of the path, + // ignore them, since intermediate components must always be + // directories. + suffixSep = newSuffixSep } if len(newparts) < i || !slices.Equal(parts[:i], newparts[:i]) { // Some component in the path which we have already traversed diff --git a/src/os/root_test.go b/src/os/root_test.go index 6f6f6cc82670d66f18bef302a5830bef8e778557..398909f8c674f04bcdd79402e6a89b7b4eac76d7 100644 --- a/src/os/root_test.go +++ b/src/os/root_test.go @@ -187,6 +187,30 @@ open: "link", target: "target", ltarget: "link", }, { + name: "symlink dotdot slash", + fs: []string{ + "link => ../", + }, + open: "link", + ltarget: "link", + wantError: true, +}, { + name: "symlink ending in slash", + fs: []string{ + "dir/", + "link => dir/", + }, + open: "link/target", + target: "dir/target", +}, { + name: "symlink dotdot dotdot slash", + fs: []string{ + "dir/link => ../../", + }, + open: "dir/link", + ltarget: "dir/link", + wantError: true, +}, { name: "symlink chain", fs: []string{ "link => a/b/c/target", @@ -213,6 +237,16 @@ "a/b/", }, open: "a/../a/b/../../a/b/../b/target", target: "a/b/target", +}, { + name: "path with dotdot slash", + fs: []string{}, + open: "../", + wantError: true, +}, { + name: "path with dotdot dotdot slash", + fs: []string{}, + open: "a/../../", + wantError: true, }, { name: "dotdot no symlink", fs: []string{