From ed76e9674926aa51129c488b7f4bbc648bca7097 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 19 Aug 2026 13:13:42 -0400 Subject: [PATCH] fix: panic when scanning squashfs symlinks (#5119) Signed-off-by: Keith Zantow --- syft/source/snapsource/snap_source.go | 22 ++--- syft/source/snapsource/snap_source_test.go | 107 +++++++++++++++++++++ 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/syft/source/snapsource/snap_source.go b/syft/source/snapsource/snap_source.go index 962e57e3c..38ad3a16d 100644 --- a/syft/source/snapsource/snap_source.go +++ b/syft/source/snapsource/snap_source.go @@ -314,19 +314,7 @@ func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *i prog.AtomicStage.Set(path) - var f filesystem.File var mimeType string - var err error - - if !d.IsDir() { - f, err = fsys.OpenFile(intFile.ToFSPath(path), os.O_RDONLY) - if err != nil { - log.WithFields("error", err, "path", path).Trace("unable to open squash file path") - } else { - defer f.Close() - mimeType = stereoFile.MIMEType(f) - } - } var ty stereoFile.Type var linkPath string @@ -335,11 +323,21 @@ func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *i // in some implementations, the mode does not indicate a directory, so we check the FileInfo type explicitly ty = stereoFile.TypeDirectory default: + f, err := fsys.OpenFile(intFile.ToFSPath(path), os.O_RDONLY) + if err != nil { + log.WithFields("error", err, "path", path).Trace("unable to open squash file path") + } + if f != nil { + defer f.Close() + } + ty = stereoFile.TypeFromMode(d.Mode()) if ty == stereoFile.TypeSymLink && f != nil { if l, ok := f.(linker); ok { linkPath, _ = l.Readlink() } + } else { + mimeType = stereoFile.MIMEType(f) } } diff --git a/syft/source/snapsource/snap_source_test.go b/syft/source/snapsource/snap_source_test.go index 4515f9c47..6a4b23e4e 100644 --- a/syft/source/snapsource/snap_source_test.go +++ b/syft/source/snapsource/snap_source_test.go @@ -1,23 +1,31 @@ package snapsource import ( + "bytes" "crypto" "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" "sync" "testing" + "time" diskFile "github.com/diskfs/go-diskfs/backend/file" + "github.com/diskfs/go-diskfs/filesystem" "github.com/diskfs/go-diskfs/filesystem/squashfs" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/wagoodman/go-progress" "github.com/anchore/go-homedir" + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/event/monitor" "github.com/anchore/syft/syft/source" ) @@ -294,3 +302,102 @@ func writeSquashfs(t *testing.T, manifest string) string { require.NoError(t, fs.Finalize(squashfs.FinalizeOptions{})) return snapPath } + +func Test_SquashfsSymlinkCrash(t *testing.T) { + tests := []struct { + name string + path string + info fakeFileInfo + file *fakeSquashFile + wantType stereoFile.Type + wantLinkDest string + wantMIME assert.ValueAssertionFunc + }{ + { + // regression: previously the visitor opened every non-dir file and computed its MIME type + // before inspecting the type, which consumed the symlink's reader so Readlink returned + // nothing and the symlink was given a bogus MIME type. The fix only reads the link for + // symlinks and only computes a MIME type for everything else. + name: "symlink resolves link destination and is not given a mime type", + path: "/link", + info: fakeFileInfo{name: "link", mode: os.ModeSymlink | 0o777}, + // the reader returns content so that the (buggy) behavior of computing a MIME type would + // have produced a non-empty MIME type, making this test fail against the old code. + file: &fakeSquashFile{Reader: bytes.NewReader([]byte("/usr/bin/target")), linkTarget: "/usr/bin/target"}, + wantType: stereoFile.TypeSymLink, + wantLinkDest: "/usr/bin/target", + wantMIME: assert.Empty, + }, + { + name: "regular file is given a mime type", + path: "/file.txt", + info: fakeFileInfo{name: "file.txt", mode: 0o644, size: 11}, + file: &fakeSquashFile{Reader: bytes.NewReader([]byte("hello world"))}, + wantType: stereoFile.TypeRegular, + wantMIME: assert.NotEmpty, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree := filetree.New() + catalog := image.NewFileCatalog() + prog := &monitor.TaskProgress{ + AtomicStage: progress.NewAtomicStage(""), + Manual: progress.NewManual(-1), + } + + fsys := fakeSquashFS{openFile: func(string, int) (filesystem.File, error) { + return tt.file, nil + }} + + visit := squashfsVisitor(tree, catalog, nil, prog) + require.NoError(t, visit(fsys, tt.path, tt.info, nil)) + + entries, err := catalog.GetByBasename(tt.info.name) + require.NoError(t, err) + require.Len(t, entries, 1) + + entry := entries[0] + assert.Equal(t, tt.wantType, entry.Metadata.Type) + assert.Equal(t, tt.wantLinkDest, entry.Metadata.LinkDestination) + tt.wantMIME(t, entry.Metadata.MIMEType) + }) + } +} + +// fakeFileInfo is a minimal os.FileInfo for driving squashfsVisitor in tests. +type fakeFileInfo struct { + name string + mode os.FileMode + size int64 +} + +func (f fakeFileInfo) Name() string { return f.name } +func (f fakeFileInfo) Size() int64 { return f.size } +func (f fakeFileInfo) Mode() os.FileMode { return f.mode } +func (f fakeFileInfo) ModTime() time.Time { return time.Time{} } +func (f fakeFileInfo) IsDir() bool { return f.mode.IsDir() } +func (f fakeFileInfo) Sys() any { return nil } + +// fakeSquashFile implements filesystem.File and the linker interface used by squashfsVisitor. +type fakeSquashFile struct { + *bytes.Reader + linkTarget string +} + +func (f *fakeSquashFile) Write([]byte) (int, error) { return 0, fs.ErrInvalid } +func (f *fakeSquashFile) Close() error { return nil } +func (f *fakeSquashFile) Stat() (fs.FileInfo, error) { return nil, fs.ErrInvalid } +func (f *fakeSquashFile) Readlink() (string, error) { return f.linkTarget, nil } + +// fakeSquashFS implements filesystem.FileSystem but only supports OpenFile; all other methods +// are inherited from the embedded nil interface and will panic if unexpectedly called. +type fakeSquashFS struct { + filesystem.FileSystem + openFile func(string, int) (filesystem.File, error) +} + +func (f fakeSquashFS) OpenFile(name string, flag int) (filesystem.File, error) { + return f.openFile(name, flag) +}