Files
buildkit/cache/contenthash/path.go
Aleksa Sarai e5171cdca5 contenthash: unify "follow" and trailing-symlink handling for Checksum
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>
2024-06-06 14:13:14 +10:00

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
}