mirror of
https://github.com/moby/buildkit.git
synced 2026-06-24 08:47:57 +00:00
This patch is part of a series which fixes the symlink resolution semantics within BuildKit. Previously, the concept of the follow flag had different meanings in various parts of the checksum codepath. FollowLinks is effectively O_NOFOLLOW, but the implementation in getFollowLinks was actually more like RESOLVE_NO_SYMLINKS. This was masked by the fact that checksumFollow would implement the O_NOFOLLOW behaviour (incorrectly), but checksumFollow would call checksumNoFollow (which would follow symlinks in path components by setting follow=true for getFollowLinks). It is much easier to simply remove these layers of indirection and unify the meaning of FollowLinks across all of the code. This means that the old follow flag is no longer needed. This also means that we can now remove the incorrect symlink resolution logic in (*cacheContext).checksumFollow() and move the followTrailing logic to (*cacheContext).checksum(), as well as removing getFollowParentLinks(). Since this removes some redundant re-checksum loops, we need to add followTrailing logic to scanPath() so that final symlink components result in the correct directory being scanned properly. The only user of (*cacheContext).checksum(follow=false) was (*cacheContext).includedPaths() which appeared to be simply using this as an optimisation (since the path being walked already had its parent path resolved). Having two easily-confused boolean flags for an optimisation that is probably not necessary (getFollowLinks already does a fast check to see if the original path is in the cache) seemed unnecessary, so just keep followTrailing. Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
// This code mostly comes from <https://github.com/cyphar/filepath-securejoin>.
|
|
|
|
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package contenthash
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var errTooManyLinks = errors.New("too many links")
|
|
|
|
const maxSymlinkLimit = 255
|
|
|
|
type onSymlinkFunc func(string, string) error
|
|
|
|
// rootPath joins a path with a root, evaluating and bounding any symlink to
|
|
// the root directory. This is a slightly modified version of SecureJoin from
|
|
// github.com/cyphar/filepath-securejoin, with a callback which we call after
|
|
// each symlink resolution.
|
|
func rootPath(root, unsafePath string, followTrailing bool, cb onSymlinkFunc) (string, error) {
|
|
if unsafePath == "" {
|
|
return root, nil
|
|
}
|
|
|
|
unsafePath = filepath.FromSlash(unsafePath)
|
|
var (
|
|
currentPath string
|
|
linksWalked int
|
|
)
|
|
for unsafePath != "" {
|
|
// Windows-specific: remove any drive letters from the path.
|
|
if v := filepath.VolumeName(unsafePath); v != "" {
|
|
unsafePath = unsafePath[len(v):]
|
|
}
|
|
|
|
// Remove any unnecessary trailing slashes.
|
|
unsafePath = strings.TrimSuffix(unsafePath, string(filepath.Separator))
|
|
|
|
// Get the next path component.
|
|
var part string
|
|
if i := strings.IndexRune(unsafePath, filepath.Separator); i == -1 {
|
|
part, unsafePath = unsafePath, ""
|
|
} else {
|
|
part, unsafePath = unsafePath[:i], unsafePath[i+1:]
|
|
}
|
|
|
|
// Apply the component lexically to the path we are building. path does
|
|
// not contain any symlinks, and we are lexically dealing with a single
|
|
// component, so it's okay to do filepath.Clean here.
|
|
nextPath := filepath.Join(string(filepath.Separator), currentPath, part)
|
|
if nextPath == string(filepath.Separator) {
|
|
// If we end up back at the root, we don't need to re-evaluate /.
|
|
currentPath = ""
|
|
continue
|
|
}
|
|
fullPath := root + string(filepath.Separator) + nextPath
|
|
|
|
// Figure out whether the path is a symlink.
|
|
fi, err := os.Lstat(fullPath)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
// Treat non-existent path components the same as non-symlinks (we
|
|
// can't do any better here).
|
|
if errors.Is(err, os.ErrNotExist) || fi.Mode()&os.ModeSymlink == 0 {
|
|
currentPath = nextPath
|
|
continue
|
|
}
|
|
// Don't resolve the final component with !followTrailing.
|
|
if !followTrailing && unsafePath == "" {
|
|
currentPath = nextPath
|
|
break
|
|
}
|
|
|
|
// It's a symlink, so get its contents and expand it by prepending it
|
|
// to the yet-unparsed path.
|
|
linksWalked++
|
|
if linksWalked > maxSymlinkLimit {
|
|
return "", errTooManyLinks
|
|
}
|
|
|
|
dest, err := os.Readlink(fullPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if cb != nil {
|
|
if err := cb(nextPath, dest); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
unsafePath = dest + string(filepath.Separator) + unsafePath
|
|
// Absolute symlinks reset any work we've already done.
|
|
if filepath.IsAbs(dest) {
|
|
currentPath = ""
|
|
}
|
|
}
|
|
|
|
// There should be no lexical components left in path here, but just for
|
|
// safety do a filepath.Clean before the join.
|
|
finalPath := filepath.Join(string(filepath.Separator), currentPath)
|
|
return filepath.Join(root, finalPath), nil
|
|
}
|