github.com/sijibomii/docker@v0.0.0-20231230191044-5cf6ca554647/pkg/symlink/fs.go (about)

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE.BSD file.
     4  
     5  // This code is a modified version of path/filepath/symlink.go from the Go standard library.
     6  
     7  package symlink
     8  
     9  import (
    10  	"bytes"
    11  	"errors"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/docker/docker/pkg/system"
    17  )
    18  
    19  // FollowSymlinkInScope is a wrapper around evalSymlinksInScope that returns an
    20  // absolute path. This function handles paths in a platform-agnostic manner.
    21  func FollowSymlinkInScope(path, root string) (string, error) {
    22  	path, err := filepath.Abs(filepath.FromSlash(path))
    23  	if err != nil {
    24  		return "", err
    25  	}
    26  	root, err = filepath.Abs(filepath.FromSlash(root))
    27  	if err != nil {
    28  		return "", err
    29  	}
    30  	return evalSymlinksInScope(path, root)
    31  }
    32  
    33  // evalSymlinksInScope will evaluate symlinks in `path` within a scope `root` and return
    34  // a result guaranteed to be contained within the scope `root`, at the time of the call.
    35  // Symlinks in `root` are not evaluated and left as-is.
    36  // Errors encountered while attempting to evaluate symlinks in path will be returned.
    37  // Non-existing paths are valid and do not constitute an error.
    38  // `path` has to contain `root` as a prefix, or else an error will be returned.
    39  // Trying to break out from `root` does not constitute an error.
    40  //
    41  // Example:
    42  //   If /foo/bar -> /outside,
    43  //   FollowSymlinkInScope("/foo/bar", "/foo") == "/foo/outside" instead of "/oustide"
    44  //
    45  // IMPORTANT: it is the caller's responsibility to call evalSymlinksInScope *after* relevant symlinks
    46  // are created and not to create subsequently, additional symlinks that could potentially make a
    47  // previously-safe path, unsafe. Example: if /foo/bar does not exist, evalSymlinksInScope("/foo/bar", "/foo")
    48  // would return "/foo/bar". If one makes /foo/bar a symlink to /baz subsequently, then "/foo/bar" should
    49  // no longer be considered safely contained in "/foo".
    50  func evalSymlinksInScope(path, root string) (string, error) {
    51  	root = filepath.Clean(root)
    52  	if path == root {
    53  		return path, nil
    54  	}
    55  	if !strings.HasPrefix(path, root) {
    56  		return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root)
    57  	}
    58  	const maxIter = 255
    59  	originalPath := path
    60  	// given root of "/a" and path of "/a/b/../../c" we want path to be "/b/../../c"
    61  	path = path[len(root):]
    62  	if root == string(filepath.Separator) {
    63  		path = string(filepath.Separator) + path
    64  	}
    65  	if !strings.HasPrefix(path, string(filepath.Separator)) {
    66  		return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root)
    67  	}
    68  	path = filepath.Clean(path)
    69  	// consume path by taking each frontmost path element,
    70  	// expanding it if it's a symlink, and appending it to b
    71  	var b bytes.Buffer
    72  	// b here will always be considered to be the "current absolute path inside
    73  	// root" when we append paths to it, we also append a slash and use
    74  	// filepath.Clean after the loop to trim the trailing slash
    75  	for n := 0; path != ""; n++ {
    76  		if n > maxIter {
    77  			return "", errors.New("evalSymlinksInScope: too many links in " + originalPath)
    78  		}
    79  
    80  		// find next path component, p
    81  		i := strings.IndexRune(path, filepath.Separator)
    82  		var p string
    83  		if i == -1 {
    84  			p, path = path, ""
    85  		} else {
    86  			p, path = path[:i], path[i+1:]
    87  		}
    88  
    89  		if p == "" {
    90  			continue
    91  		}
    92  
    93  		// this takes a b.String() like "b/../" and a p like "c" and turns it
    94  		// into "/b/../c" which then gets filepath.Cleaned into "/c" and then
    95  		// root gets prepended and we Clean again (to remove any trailing slash
    96  		// if the first Clean gave us just "/")
    97  		cleanP := filepath.Clean(string(filepath.Separator) + b.String() + p)
    98  		if cleanP == string(filepath.Separator) {
    99  			// never Lstat "/" itself
   100  			b.Reset()
   101  			continue
   102  		}
   103  		fullP := filepath.Clean(root + cleanP)
   104  
   105  		fi, err := os.Lstat(fullP)
   106  		if os.IsNotExist(err) {
   107  			// if p does not exist, accept it
   108  			b.WriteString(p)
   109  			b.WriteRune(filepath.Separator)
   110  			continue
   111  		}
   112  		if err != nil {
   113  			return "", err
   114  		}
   115  		if fi.Mode()&os.ModeSymlink == 0 {
   116  			b.WriteString(p + string(filepath.Separator))
   117  			continue
   118  		}
   119  
   120  		// it's a symlink, put it at the front of path
   121  		dest, err := os.Readlink(fullP)
   122  		if err != nil {
   123  			return "", err
   124  		}
   125  		if system.IsAbs(dest) {
   126  			b.Reset()
   127  		}
   128  		path = dest + string(filepath.Separator) + path
   129  	}
   130  
   131  	// see note above on "fullP := ..." for why this is double-cleaned and
   132  	// what's happening here
   133  	return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil
   134  }
   135  
   136  // EvalSymlinks returns the path name after the evaluation of any symbolic
   137  // links.
   138  // If path is relative the result will be relative to the current directory,
   139  // unless one of the components is an absolute symbolic link.
   140  // This version has been updated to support long paths prepended with `\\?\`.
   141  func EvalSymlinks(path string) (string, error) {
   142  	return evalSymlinks(path)
   143  }