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
7package symlink // import "github.com/docker/docker/pkg/symlink"
8
9import (
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.
21func 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 "/outside"
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".
50func 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 isDriveOrRoot(cleanP) {
99			// never Lstat "/" itself, or drive letters on Windows
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)
117			b.WriteRune(filepath.Separator)
118			continue
119		}
120
121		// it's a symlink, put it at the front of path
122		dest, err := os.Readlink(fullP)
123		if err != nil {
124			return "", err
125		}
126		if system.IsAbs(dest) {
127			b.Reset()
128		}
129		path = dest + string(filepath.Separator) + path
130	}
131
132	// see note above on "fullP := ..." for why this is double-cleaned and
133	// what's happening here
134	return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil
135}
136
137// EvalSymlinks returns the path name after the evaluation of any symbolic
138// links.
139// If path is relative the result will be relative to the current directory,
140// unless one of the components is an absolute symbolic link.
141// This version has been updated to support long paths prepended with `\\?\`.
142func EvalSymlinks(path string) (string, error) {
143	return evalSymlinks(path)
144}
145