fix(general): prevent infinite loop while resolving ignore file symlinks (#4413)

* test symlink infinite loop
* remove spurious error wrap
* cleanup error messages
* remove unneeded intermediate vars
* check error in loop in fs.GetAllEntries
  While cur is expected to be `nil` when there's an error, this
  makes the check explicit.

Ref:
- #2037
- #4190
This commit is contained in:
Julio López
2025-02-14 23:38:32 -08:00
committed by GitHub
parent 73a70fcd6b
commit a394a5029f
4 changed files with 44 additions and 14 deletions

View File

@@ -72,19 +72,15 @@ type Directory interface {
func IterateEntries(ctx context.Context, dir Directory, cb func(context.Context, Entry) error) error {
iter, err := dir.Iterate(ctx)
if err != nil {
return errors.Wrapf(err, "in fs.IterateEntries, creating iterator for directory %s", dir.Name())
return errors.Wrapf(err, "cannot iterate directory '%q'", dir.Name())
}
defer iter.Close()
cur, err := iter.Next(ctx)
if err != nil {
err = errors.Wrapf(err, "in fs.IterateEntries, on first iteration")
}
for cur != nil {
if err2 := cb(ctx, cur); err2 != nil {
return errors.Wrapf(err2, "in fs.IterateEntries, while calling callback on file %s", cur.Name())
return errors.Wrapf(err2, "callback failed on '%q'", cur.Name())
}
cur, err = iter.Next(ctx)
@@ -136,7 +132,7 @@ func GetAllEntries(ctx context.Context, d Directory) ([]Entry, error) {
defer iter.Close()
cur, err := iter.Next(ctx)
for cur != nil {
for err == nil && cur != nil {
entries = append(entries, cur)
cur, err = iter.Next(ctx)
}

View File

@@ -20,6 +20,7 @@
var (
log = logging.Module("ignorefs")
errSymlinkNotAFile = errors.New("Symlink does not link to a file")
errTooManySymlinks = errors.New("too many levels of symbolic links")
)
// IgnoreCallback is a function called by ignorefs to report whenever a file or directory is being ignored while listing its parent.
@@ -188,12 +189,12 @@ func (d *ignoreDirectory) Iterate(ctx context.Context) (fs.DirectoryIterator, er
thisContext, err := d.buildContext(ctx)
if err != nil {
return nil, errors.Wrapf(err, "in ignoreDirectory.Iterate, when building context")
return nil, err
}
inner, err := d.Directory.Iterate(ctx)
if err != nil {
return nil, errors.Wrapf(err, "in ignoreDirectory.Iterate, when creating iterator")
return nil, errors.Wrap(err, "cannot create iterator")
}
it := ignoreDirIteratorPool.Get().(*ignoreDirIterator) //nolint:forcetypeassert
@@ -275,7 +276,9 @@ func (d *ignoreDirectory) Child(ctx context.Context, name string) (fs.Entry, err
}
func resolveSymlink(ctx context.Context, entry fs.Symlink) (fs.File, error) {
for {
const maxSymlinkFollow = 30
for range maxSymlinkFollow {
target, err := entry.Resolve(ctx)
if err != nil {
link, _ := entry.Readlink(ctx)
@@ -292,6 +295,8 @@ func resolveSymlink(ctx context.Context, entry fs.Symlink) (fs.File, error) {
return nil, errors.Wrapf(errSymlinkNotAFile, "%s does not eventually link to a file", entry.Name())
}
}
return nil, errors.Wrapf(errTooManySymlinks, "cannot resolve '%q'", entry.Name())
}
func (d *ignoreDirectory) buildContext(ctx context.Context) (*ignoreContext, error) {

View File

@@ -0,0 +1,31 @@
package ignorefs
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/internal/mockfs"
"github.com/kopia/kopia/internal/testlogging"
)
func TestNoInfiniteResolveLink(t *testing.T) {
root := mockfs.NewDirectory()
root.AddSymlink("a", "./b", 0)
root.AddSymlink("b", "./c", 0)
root.AddSymlink("c", "./a", 0)
ctx := testlogging.Context(t)
e, err := root.Child(ctx, "b")
require.NoError(t, err)
s, ok := e.(fs.Symlink)
require.True(t, ok)
f, err := resolveSymlink(ctx, s)
require.ErrorIs(t, err, errTooManySymlinks)
require.Nil(t, f)
}

View File

@@ -120,12 +120,10 @@ func (fsl *filesystemSymlink) Readlink(ctx context.Context) (string, error) {
func (fsl *filesystemSymlink) Resolve(ctx context.Context) (fs.Entry, error) {
target, err := filepath.EvalSymlinks(fsl.fullPath())
if err != nil {
return nil, errors.Wrapf(err, "while reading symlink %s", fsl.fullPath())
return nil, errors.Wrapf(err, "cannot resolve symlink for '%q'", fsl.fullPath())
}
entry, err := NewEntry(target)
return entry, err
return NewEntry(target)
}
func (e *filesystemErrorEntry) ErrorInfo() error {