Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix link cycles with non-existent paths #148

Merged
merged 1 commit into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions pkg/filetree/filetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (t *FileTree) node(p file.Path, strategy linkResolutionStrategy) (*filenode
var currentNode *filenode.FileNode
var err error
if strategy.FollowAncestorLinks {
currentNode, err = t.resolveAncestorLinks(normalizedPath)
currentNode, err = t.resolveAncestorLinks(normalizedPath, nil)
if err != nil {
return currentNode, err
}
Expand All @@ -185,14 +185,14 @@ func (t *FileTree) node(p file.Path, strategy linkResolutionStrategy) (*filenode
}

if strategy.FollowBasenameLinks {
currentNode, err = t.resolveNodeLinks(currentNode, !strategy.DoNotFollowDeadBasenameLinks)
currentNode, err = t.resolveNodeLinks(currentNode, !strategy.DoNotFollowDeadBasenameLinks, nil)
}
return currentNode, err
}

// return FileNode of the basename in the given path (no resolution is done at or past the basename). Note: it is
// assumed that the given path has already been normalized.
func (t *FileTree) resolveAncestorLinks(path file.Path) (*filenode.FileNode, error) {
func (t *FileTree) resolveAncestorLinks(path file.Path, attemptedPaths internal.Set) (*filenode.FileNode, error) {
// performance optimization... see if there is a node at the path (as if it is a real path). If so,
// use it, otherwise, continue with ancestor resolution
currentNode, err := t.node(path, linkResolutionStrategy{})
Expand Down Expand Up @@ -248,7 +248,7 @@ func (t *FileTree) resolveAncestorLinks(path file.Path) (*filenode.FileNode, err
// links until the next Node is resolved (or not).
isLastPart := idx == len(pathParts)-1
if !isLastPart && currentNode.IsLink() {
currentNode, err = t.resolveNodeLinks(currentNode, true)
currentNode, err = t.resolveNodeLinks(currentNode, true, attemptedPaths)
if err != nil {
// only expected to happen on cycles
return currentNode, err
Expand All @@ -266,11 +266,16 @@ func (t *FileTree) resolveAncestorLinks(path file.Path) (*filenode.FileNode, err

// followNode takes the given FileNode and resolves all links at the base of the real path for the node (this implies
// that NO ancestors are considered).
func (t *FileTree) resolveNodeLinks(n *filenode.FileNode, followDeadBasenameLinks bool) (*filenode.FileNode, error) {
func (t *FileTree) resolveNodeLinks(n *filenode.FileNode, followDeadBasenameLinks bool, attemptedPaths internal.Set) (*filenode.FileNode, error) {
if n == nil {
return nil, fmt.Errorf("cannot resolve links with nil Node given")
}

// we need to short-circuit link resolution that never resolves (cycles) due to a cycle referencing nodes that do not exist
if attemptedPaths == nil {
attemptedPaths = internal.NewStringSet()
}

// note: this assumes that callers are passing paths in which the constituent parts are NOT symlinks
var lastNode *filenode.FileNode

Expand Down Expand Up @@ -318,8 +323,14 @@ func (t *FileTree) resolveNodeLinks(n *filenode.FileNode, followDeadBasenameLink
// preserve the current Node for the next loop (in case we shouldn't follow a potentially dead link)
lastNode = currentNode

// break any cycles with non-existent paths (before attempting to look the path up again)
if attemptedPaths.Contains(string(nextPath)) {
return nil, ErrLinkCycleDetected
}

// get the next Node (based on the next path)
currentNode, err = t.resolveAncestorLinks(nextPath)
attemptedPaths.Add(string(nextPath))
currentNode, err = t.resolveAncestorLinks(nextPath, attemptedPaths)
if err != nil {
// only expected to occur upon cycle detection
return currentNode, err
Expand Down
18 changes: 18 additions & 0 deletions pkg/filetree/filetree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package filetree
import (
"errors"
"fmt"
"github.com/stretchr/testify/require"
"testing"

"github.com/anchore/stereoscope/internal"
Expand Down Expand Up @@ -787,6 +788,23 @@ func TestFileTree_File_CycleDetection(t *testing.T) {

}

func TestFileTree_File_DeadCycleDetection(t *testing.T) {
tr := NewFileTree()
_, err := tr.AddSymLink("/somewhere/acorn", "noobaa-core/../acorn/bin/acorn")
require.NoError(t, err)

// the test.... do we stop when a cycle is detected?
exists, _, err := tr.File("/somewhere/acorn", FollowBasenameLinks)
if err != ErrLinkCycleDetected {
t.Fatalf("should have gotten an error on resolving a file")
}

if exists {
t.Errorf("resolution should not exist in cycle")
}

}

func TestFileTree_AllFiles(t *testing.T) {
tr := NewFileTree()

Expand Down