mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 15:36:41 +01:00
fix: don't fail the whole SBOM when a cataloger panics on one file (#5342)
* fix(generic): recover from parser and processor panics - a panic while parsing one file becomes an unknown for that location instead of failing the whole SBOM - a panic in a package processor is logged as a warning and the unprocessed results are kept - stacks go to the trace log, not into the unknown text Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(unknown): keep errors that unwrap to nil `sync.PanicError` with a non-error panic value (e.g. `panic("boom")`) unwraps to nil, and was being dropped entirely from the error graph instead of being kept as a leaf. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(task): don't put the stack trace in task panic errors Task panics are still fatal, but the error now reads `panic in task "<name>": <value>` and the stack goes to the debug log. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test(generic): cover chained processors when a later one panics Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(file): recover from panics in the executable and file digest catalogers A panic while reading one file is now recorded as an unknown for that file instead of failing the whole SBOM, the same as generic catalogers. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package generic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/anchore/go-logger"
|
||||
"github.com/anchore/go-sync"
|
||||
@@ -221,12 +223,24 @@ func (c *Cataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.
|
||||
|
||||
func (c *Cataloger) process(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
for _, p := range c.processors {
|
||||
pkgs, rels, err = p.process(ctx, resolver, pkgs, rels, err)
|
||||
pkgs, rels, err = c.processSafely(ctx, p, resolver, pkgs, rels, err)
|
||||
}
|
||||
return pkgs, rels, err
|
||||
}
|
||||
|
||||
func invokeParser(ctx context.Context, resolver file.Resolver, location file.Location, logger logger.Logger, parser Parser, env *Environment) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func (c *Cataloger) processSafely(ctx context.Context, p processExecutor, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) (outPkgs []pkg.Package, outRels []artifact.Relationship, outErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// keep the unprocessed results; there is no single location to attach an unknown to
|
||||
log.WithFields("cataloger", c.upstreamCataloger, "panic", r).Warn("recovered from panic in package processor")
|
||||
log.Tracef("processor panic stack:\n%s", debug.Stack())
|
||||
outPkgs, outRels, outErr = pkgs, rels, err
|
||||
}
|
||||
}()
|
||||
return p.process(ctx, resolver, pkgs, rels, err)
|
||||
}
|
||||
|
||||
func invokeParser(ctx context.Context, resolver file.Resolver, location file.Location, logger logger.Logger, parser Parser, env *Environment) (pkgs []pkg.Package, rels []artifact.Relationship, err error) {
|
||||
contentReader, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
logger.WithFields("location", location.RealPath, "error", err).Debug("unable to fetch contents")
|
||||
@@ -234,6 +248,15 @@ func invokeParser(ctx context.Context, resolver file.Resolver, location file.Loc
|
||||
}
|
||||
defer internal.CloseAndLogError(contentReader, location.AccessPath)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// a malformed file must not take down the whole SBOM; the caller records this as an unknown
|
||||
logger.WithFields("location", location.RealPath, "panic", r).Debug("recovered from panic while parsing file")
|
||||
logger.Tracef("parser panic stack:\n%s", debug.Stack())
|
||||
pkgs, rels, err = nil, nil, fmt.Errorf("recovered from panic while parsing file: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
discoveredPackages, discoveredRelationships, err := parser(ctx, resolver, env, file.NewLocationReadCloser(location, contentReader))
|
||||
if err != nil {
|
||||
// these errors are propagated up, and are likely to be coordinate errors
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/go-sync"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -190,12 +189,122 @@ func TestClosesFileOnParserPanic(t *testing.T) {
|
||||
|
||||
_, _, err := c.Catalog(ctx, resolver)
|
||||
require.Error(t, err)
|
||||
var panicErr sync.PanicError
|
||||
require.ErrorAs(t, err, &panicErr)
|
||||
assert.Equal(t, "panic!", panicErr.Value)
|
||||
var coordErr *unknown.CoordinateError
|
||||
require.ErrorAs(t, err, &coordErr)
|
||||
assert.ErrorContains(t, err, "recovered from panic while parsing file: panic!")
|
||||
require.True(t, spy.closed)
|
||||
}
|
||||
|
||||
func TestParserPanicBecomesUnknown(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
doPanic func()
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "string panic",
|
||||
doPanic: func() { panic("boom") },
|
||||
wantErr: "boom",
|
||||
},
|
||||
{
|
||||
name: "runtime panic",
|
||||
doPanic: func() {
|
||||
var m map[string]int
|
||||
m["x"] = 1
|
||||
},
|
||||
wantErr: "assignment to entry in nil map",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := func(_ context.Context, _ file.Resolver, _ *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
if reader.Path() == "testdata/another-path.txt" {
|
||||
tt.doPanic()
|
||||
}
|
||||
return []pkg.Package{{Name: "good", Locations: file.NewLocationSet(reader.Location)}}, nil, nil
|
||||
}
|
||||
|
||||
resolver := file.NewMockResolverForPaths("testdata/a-path.txt", "testdata/another-path.txt")
|
||||
c := NewCataloger("unit-test-cataloger").
|
||||
WithParserByPath(parser, "testdata/a-path.txt", "testdata/another-path.txt")
|
||||
|
||||
pkgs, _, err := c.Catalog(context.Background(), resolver)
|
||||
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, "good", pkgs[0].Name)
|
||||
|
||||
unknowns, remaining := unknown.ExtractCoordinateErrors(err)
|
||||
require.NoError(t, remaining)
|
||||
require.Len(t, unknowns, 1)
|
||||
assert.Equal(t, "testdata/another-path.txt", unknowns[0].Coordinates.RealPath)
|
||||
assert.ErrorContains(t, unknowns[0].Reason, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessorPanicKeepsResults(t *testing.T) {
|
||||
parser := func(_ context.Context, _ file.Resolver, _ *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
return []pkg.Package{{Name: "good", Locations: file.NewLocationSet(reader.Location)}}, nil, nil
|
||||
}
|
||||
|
||||
rename := func(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
for i := range pkgs {
|
||||
pkgs[i].Name = "processed"
|
||||
}
|
||||
return pkgs, rels, err
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cataloger *Cataloger
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "processor",
|
||||
cataloger: NewCataloger("unit-test-cataloger").
|
||||
WithParserByPath(parser, "testdata/a-path.txt").
|
||||
WithProcessors(func([]pkg.Package, []artifact.Relationship, error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
panic("boom")
|
||||
}),
|
||||
want: "good",
|
||||
},
|
||||
{
|
||||
// output from earlier processors survives a later processor panicking
|
||||
name: "chained processors",
|
||||
cataloger: NewCataloger("unit-test-cataloger").
|
||||
WithParserByPath(parser, "testdata/a-path.txt").
|
||||
WithProcessors(rename, func([]pkg.Package, []artifact.Relationship, error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
panic("boom")
|
||||
}),
|
||||
want: "processed",
|
||||
},
|
||||
{
|
||||
name: "resolving processor",
|
||||
cataloger: NewCataloger("unit-test-cataloger").
|
||||
WithParserByPath(parser, "testdata/a-path.txt").
|
||||
WithResolvingProcessors(func(context.Context, file.Resolver, []pkg.Package, []artifact.Relationship, error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var m map[string]int
|
||||
m["x"] = 1
|
||||
return nil, nil, nil
|
||||
}),
|
||||
want: "good",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resolver := file.NewMockResolverForPaths("testdata/a-path.txt")
|
||||
|
||||
pkgs, _, err := tt.cataloger.Catalog(context.Background(), resolver)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, tt.want, pkgs[0].Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CatalogerWithParserByMediaType(t *testing.T) {
|
||||
allParsedPaths := make(map[string]bool)
|
||||
parser := func(_ context.Context, resolver file.Resolver, env *Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
|
||||
Reference in New Issue
Block a user