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