@@ -153,11 +153,23 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
153153 return tailEntry .dir , tailEntry .remainingPath , true
154154}
155155
156+ const maxUnsafeHallucinateDirectoryTries = 20
157+
158+ var errTooManyFakeDirectories = errors .New ("encountered too many non-existent paths" )
159+
156160// partialLookupInRoot tries to lookup as much of the request path as possible
157161// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
158162// component of the requested path, returning a file handle to the final
159163// existing component and a string containing the remaining path components.
160- func partialLookupInRoot (root * os.File , unsafePath string ) (_ * os.File , _ string , Err error ) {
164+ //
165+ // If unsafeHallucinateDirectories is true, partialLookupInRoot will try to
166+ // emulate the legacy SecureJoin behaviour of treating non-existent paths as
167+ // though they are directories to try to resolve as much of the path as
168+ // possible. In practice, this means that a path like "a/b/doesnotexist/../c"
169+ // will end up being resolved as "a/b/c" if possible. Note that dangling
170+ // symlinks (a symlink that points to a non-existent path) will still result in
171+ // an error being returned, due to how openat2 handles symlinks.
172+ func partialLookupInRoot (root * os.File , unsafePath string , unsafeHallucinateDirectories bool ) (_ * os.File , _ string , Err error ) {
161173 unsafePath = filepath .ToSlash (unsafePath ) // noop
162174
163175 // This is very similar to SecureJoin, except that we operate on the
@@ -166,7 +178,7 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string
166178
167179 // Try to use openat2 if possible.
168180 if hasOpenat2 () {
169- return partialLookupOpenat2 (root , unsafePath )
181+ return partialLookupOpenat2 (root , unsafePath , unsafeHallucinateDirectories )
170182 }
171183
172184 // Get the "actual" root path from /proc/self/fd. This is necessary if the
@@ -204,7 +216,9 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string
204216 defer symlinkStack .Close ()
205217
206218 var (
207- linksWalked int
219+ linksWalked int
220+ hallucinateDirectoryTries int
221+
208222 currentPath string
209223 remainingPath = unsafePath
210224 )
@@ -354,6 +368,21 @@ func partialLookupInRoot(root *os.File, unsafePath string) (_ *os.File, _ string
354368 _ = currentDir .Close ()
355369 return oldDir , remainingPath , nil
356370 }
371+ // If we were asked to "hallucinate" non-existent paths as though
372+ // they are directories, take the remainingPath and clean it so
373+ // that any ".." components that would lead us back to real paths
374+ // can get resolved.
375+ if oldRemainingPath != "" && unsafeHallucinateDirectories {
376+ if newRemainingPath := path .Clean (oldRemainingPath ); newRemainingPath != oldRemainingPath {
377+ hallucinateDirectoryTries ++
378+ if hallucinateDirectoryTries > maxUnsafeHallucinateDirectoryTries {
379+ return nil , "" , fmt .Errorf ("%w: trying to reconcile non-existent subpath %q" , errTooManyFakeDirectories , oldRemainingPath )
380+ }
381+ // Continue the lookup using the new remaining path.
382+ remainingPath = newRemainingPath
383+ continue
384+ }
385+ }
357386 // We have hit a final component that doesn't exist, so we have our
358387 // partial open result. Note that we have to use the OLD remaining
359388 // path, since the lookup failed.
0 commit comments