Skip to content

Commit 4c96129

Browse files
kevglynncursoragent
andcommitted
fix(fs): skip unreadable files and directories during filesystem walk
Unreadable files and directories (e.g. "lstat: input/output error" on special files when scanning a mounted Windows filesystem) currently abort the whole scan. Skip them with a debug log instead, as they cannot be analyzed anyway. Errors with the root directory itself are still returned as the scan cannot continue without it. Close #3259 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aff9078 commit 4c96129

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

pkg/fanal/walker/fs.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ func (w *FS) Walk(root string, opt Option, fn WalkFunc) error {
4141
func (w *FS) WalkDirFunc(root string, fn WalkFunc, opt Option) fs.WalkDirFunc {
4242
return func(filePath string, d fs.DirEntry, err error) error {
4343
if err != nil {
44-
return err
44+
// An error with the root itself must be returned as the scan cannot continue,
45+
// but permission errors are still handled in `onError`.
46+
if filePath == root {
47+
return err
48+
}
49+
// Skip unreadable files and directories instead of aborting the whole scan
50+
// as they cannot be analyzed anyway (e.g. "lstat: input/output error" on special files).
51+
// cf. https://github.com/aquasecurity/trivy/issues/3259
52+
log.Debug("Skipping unreadable path", log.FilePath(filePath), log.Err(err))
53+
return nil
4554
}
4655

4756
// For exported rootfs (e.g. images/alpine/etc/alpine-release)
@@ -66,7 +75,11 @@ func (w *FS) WalkDirFunc(root string, fn WalkFunc, opt Option) fs.WalkDirFunc {
6675

6776
info, err := d.Info()
6877
if err != nil {
69-
return xerrors.Errorf("file info error: %w", err)
78+
// Skip unreadable files instead of aborting the whole scan
79+
// (e.g. "lstat: input/output error" on special files).
80+
// cf. https://github.com/aquasecurity/trivy/issues/3259
81+
log.Debug("Skipping unreadable file", log.FilePath(filePath), log.Err(err))
82+
return nil
7083
}
7184

7285
if err = fn(relPath, info, fileOpener(filePath)); err != nil {

pkg/fanal/walker/fs_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package walker_test
33
import (
44
"errors"
55
"io"
6+
"io/fs"
67
"os"
78
"path/filepath"
89
"runtime"
910
"slices"
1011
"strings"
12+
"syscall"
1113
"testing"
1214

1315
"github.com/stretchr/testify/assert"
@@ -89,6 +91,58 @@ func TestFS_Walk(t *testing.T) {
8991
}
9092
}
9193

94+
// fakeDirEntry is a fs.DirEntry that fails on Info().
95+
type fakeDirEntry struct {
96+
infoErr error
97+
}
98+
99+
func (e fakeDirEntry) Name() string { return "bad" }
100+
func (e fakeDirEntry) IsDir() bool { return false }
101+
func (e fakeDirEntry) Type() fs.FileMode { return 0 } // regular file
102+
func (e fakeDirEntry) Info() (fs.FileInfo, error) { return nil, e.infoErr }
103+
104+
func TestFS_WalkDirFunc_UnreadablePaths(t *testing.T) {
105+
root := "testdata/fs"
106+
analyzeFn := func(string, os.FileInfo, analyzer.Opener) error {
107+
assert.Fail(t, "analyze function must not be called for unreadable paths")
108+
return nil
109+
}
110+
walkFn := walker.NewFS().WalkDirFunc(root, analyzeFn, walker.Option{})
111+
112+
t.Run("unreadable file is skipped", func(t *testing.T) {
113+
// e.g. "lstat: input/output error" on a broken filesystem
114+
// cf. https://github.com/aquasecurity/trivy/issues/3259
115+
err := walkFn(filepath.Join(root, "bad"), nil, &fs.PathError{
116+
Op: "lstat",
117+
Path: filepath.Join(root, "bad"),
118+
Err: syscall.EIO,
119+
})
120+
assert.NoError(t, err)
121+
})
122+
123+
t.Run("error with the root is returned", func(t *testing.T) {
124+
err := walkFn(root, nil, &fs.PathError{
125+
Op: "lstat",
126+
Path: root,
127+
Err: syscall.EIO,
128+
})
129+
assert.ErrorIs(t, err, syscall.EIO)
130+
})
131+
132+
t.Run("file with unreadable info is skipped", func(t *testing.T) {
133+
err := walkFn(filepath.Join(root, "bad"), fakeDirEntry{infoErr: syscall.EIO}, nil)
134+
assert.NoError(t, err)
135+
})
136+
}
137+
138+
func TestFS_Walk_NonExistentRoot(t *testing.T) {
139+
analyzeFn := func(string, os.FileInfo, analyzer.Opener) error {
140+
return nil
141+
}
142+
err := walker.NewFS().Walk("testdata/non-existent", walker.Option{}, analyzeFn)
143+
assert.ErrorContains(t, err, "unknown error with")
144+
}
145+
92146
func TestFS_BuildSkipPaths(t *testing.T) {
93147
tests := []struct {
94148
name string

0 commit comments

Comments
 (0)