Skip to content

Commit

Permalink
contenthash: implement proper Linux symlink semantics for needsScan
Browse files Browse the repository at this point in the history
This patch is part of a series which fixes the symlink resolution
semantics within BuildKit.

Since we now have a working implementation of symlink resolution in
getFollowLinks, we can add a callback after each component is resolved
so that we can track which components were and were not found during the
resolution. Because getFollowLinks resolves the components in-order, we
can use this to tell needsScan whether the resolver found a real
(non-symlink) ancestor of the requested path in the cache.

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
  • Loading branch information
cyphar committed May 3, 2024
1 parent e9a388f commit fa77668
Showing 1 changed file with 52 additions and 30 deletions.
82 changes: 52 additions & 30 deletions cache/contenthash/checksum.go
Original file line number Diff line number Diff line change
Expand Up @@ -948,41 +948,41 @@ func (cc *cacheContext) checksum(ctx context.Context, root *iradix.Node, txn *ir
}

// needsScan returns false if path is in the tree or a parent path is in tree
// and subpath is missing
func (cc *cacheContext) needsScan(root *iradix.Node, p string) (bool, error) {
var linksWalked int
return cc.needsScanFollow(root, p, &linksWalked)
}

func (cc *cacheContext) needsScanFollow(root *iradix.Node, p string, linksWalked *int) (bool, error) {
if p == "/" {
p = ""
}
v, ok := root.Get(convertPathToKey([]byte(p)))
if !ok {
if p == "" {
return true, nil
}
return cc.needsScanFollow(root, path.Clean(path.Dir(p)), linksWalked)
}
cr := v.(*CacheRecord)
if cr.Type == CacheRecordTypeSymlink {
if *linksWalked > 255 {
return false, errTooManyLinks
}
*linksWalked++
link := path.Clean(cr.Linkname)
if !path.IsAbs(cr.Linkname) {
link = path.Join("/", path.Dir(p), link)
// and subpath is missing.
func (cc *cacheContext) needsScan(root *iradix.Node, path string) (bool, error) {
var (
lastGoodPath string
hasParentInTree bool
)
k := convertPathToKey([]byte(path))
_, cr, err := getFollowLinksCallback(root, k, true, func(subpath string, cr *CacheRecord) error {
if cr != nil {
// If the path is not a symlink, then for now we have a parent in
// the tree. Otherwise, we reset hasParentInTree because we
// might've jumped to a part of the tree that hasn't been scanned.
hasParentInTree = (cr.Type != CacheRecordTypeSymlink)
if hasParentInTree {
lastGoodPath = subpath
}
} else if hasParentInTree {
// If the current component was something like ".." and the subpath
// couldn't be found, then we need to invalidate hasParentInTree.
// In practice this means that our subpath needs to be prefixed by
// the last good path. We add a trailing slash to make sure the
// prefix is a proper lexical prefix (as opposed to /a/b being seen
// as a prefix of /a/bc).
hasParentInTree = strings.HasPrefix(subpath+"/", lastGoodPath+"/")
}
return cc.needsScanFollow(root, link, linksWalked)
return nil
})
if err != nil {
return false, err
}
return false, nil
return cr == nil && !hasParentInTree, nil
}

func (cc *cacheContext) scanPath(ctx context.Context, m *mount, p string) (retErr error) {
p = path.Join("/", p)
d, _ := path.Split(p)
d := path.Dir(path.Join("/", p))

mp, err := m.mount(ctx)
if err != nil {
Expand All @@ -1008,6 +1008,10 @@ func (cc *cacheContext) scanPath(ctx context.Context, m *mount, p string) (retEr

err = filepath.Walk(parentPath, func(itemPath string, fi os.FileInfo, err error) error {
if err != nil {
// If the root doesn't exist, ignore the error.
if errors.Is(err, os.ErrNotExist) {
return nil
}
return errors.Wrapf(err, "failed to walk %s", itemPath)
}
rel, err := filepath.Rel(mp, itemPath)
Expand Down Expand Up @@ -1079,7 +1083,15 @@ func getFollowParentLinks(root *iradix.Node, k []byte, follow bool) ([]byte, *Ca
return k, nil, nil
}

// followLinksCallback is called after we try to resolve each element. If the
// path was not found, cr is nil.
type followLinksCallback func(path string, cr *CacheRecord) error

func getFollowLinks(root *iradix.Node, k []byte, follow bool) ([]byte, *CacheRecord, error) {
return getFollowLinksCallback(root, k, follow, nil)
}

func getFollowLinksCallback(root *iradix.Node, k []byte, follow bool, cb followLinksCallback) ([]byte, *CacheRecord, error) {
v, ok := root.Get(k)
if ok && v.(*CacheRecord).Type != CacheRecordTypeSymlink {
return k, v.(*CacheRecord), nil
Expand Down Expand Up @@ -1125,6 +1137,11 @@ func getFollowLinks(root *iradix.Node, k []byte, follow bool) ([]byte, *CacheRec
if ok {
cr = v.(*CacheRecord)
}
if cb != nil {
if err := cb(nextPath, cr); err != nil {
return nil, nil, err
}
}
if !ok || cr.Type != CacheRecordTypeSymlink {
currentPath = nextPath
continue
Expand All @@ -1150,6 +1167,11 @@ func getFollowLinks(root *iradix.Node, k []byte, follow bool) ([]byte, *CacheRec
if ok {
cr = v.(*CacheRecord)
}
if cb != nil {
if err := cb(currentPath, cr); err != nil {
return nil, nil, err
}
}
}
return convertPathToKey([]byte(currentPath)), cr, nil
}
Expand Down

0 comments on commit fa77668

Please sign in to comment.