fix: panic when scanning squashfs symlinks (#5119)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow 2026-08-19 13:13:42 -04:00 committed by GitHub
parent ab508169e6
commit ed76e96749
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 117 additions and 12 deletions

View File

@ -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)
}
}

View File

@ -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)
}