Skip to content

Commit 3054b3b

Browse files
authored
fix: surface the original analysis error instead of context cancellation (aquasecurity#10793)
1 parent 15bdd2c commit 3054b3b

5 files changed

Lines changed: 69 additions & 15 deletions

File tree

pkg/fanal/analyzer/analyzer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ func (ag AnalyzerGroup) AnalyzeFile(ctx context.Context, eg *errgroup.Group, lim
490490
}
491491

492492
if err = limit.Acquire(ctx, 1); err != nil {
493+
// The goroutine below (which closes rc) is not started on this path,
494+
// so close the opened file here to avoid leaking the handle.
495+
_ = rc.Close()
493496
return xerrors.Errorf("semaphore acquire: %w", err)
494497
}
495498

pkg/fanal/artifact/image/image.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ func (a Artifact) inspectLayer(ctx context.Context, layer types.Layer, disabled
465465
defer composite.Cleanup()
466466

467467
// Walk a tar layer
468-
opqDirs, whFiles, err := a.walker.Walk(cr, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
469-
if err = a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "", filePath, info, opener, disabled, opts); err != nil {
468+
opqDirs, whFiles, walkErr := a.walker.Walk(cr, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
469+
if err := a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "", filePath, info, opener, disabled, opts); err != nil {
470470
return xerrors.Errorf("failed to analyze %s: %w", filePath, err)
471471
}
472472

@@ -487,14 +487,16 @@ func (a Artifact) inspectLayer(ctx context.Context, layer types.Layer, disabled
487487

488488
return nil
489489
})
490-
if err != nil {
491-
return types.BlobInfo{}, xerrors.Errorf("walk error: %w", err)
492-
}
493490

494-
// Wait for all the goroutine to finish and check errors
491+
// errgroup cancels egCtx when an analysis goroutine fails, so the walk above can
492+
// fail with context.Canceled and mask the real cause (e.g. a remote 429).
493+
// Surface eg.Wait()'s error first; fall back to the walk error only when the group is clean.
495494
if err = eg.Wait(); err != nil {
496495
return types.BlobInfo{}, xerrors.Errorf("analyze error: %w", err)
497496
}
497+
if walkErr != nil {
498+
return types.BlobInfo{}, xerrors.Errorf("walk error: %w", walkErr)
499+
}
498500

499501
// Post-analysis
500502
if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil {

pkg/fanal/artifact/local/fs.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,23 +212,29 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
212212

213213
// Use static paths instead of traversing the filesystem when all analyzers implement StaticPathAnalyzer
214214
// so that we can analyze files faster
215+
var walkErr error
215216
if paths, canUseStaticPaths := a.analyzer.StaticPaths(a.artifactOption.DisabledAnalyzers); canUseStaticPaths {
216217
// Analyze files in static paths
217218
a.logger.Debug("Analyzing files in static paths")
218219
if err = a.analyzeWithStaticPaths(egCtx, eg, limit, result, composite, opts, paths); err != nil {
219-
return artifact.Reference{}, xerrors.Errorf("analyze with static paths: %w", err)
220+
walkErr = xerrors.Errorf("analyze with static paths: %w", err)
220221
}
221222
} else {
222223
// Analyze files by traversing the root directory
223224
if err = a.analyzeWithRootDir(egCtx, eg, limit, result, composite, opts); err != nil {
224-
return artifact.Reference{}, xerrors.Errorf("analyze with traversal: %w", err)
225+
walkErr = xerrors.Errorf("analyze with traversal: %w", err)
225226
}
226227
}
227228

228-
// Wait for all the goroutine to finish.
229+
// errgroup cancels egCtx when an analysis goroutine fails, so the walk above can
230+
// fail with context.Canceled and mask the real cause (e.g. a remote 429).
231+
// Surface eg.Wait()'s error first; fall back to the walk error only when the group is clean.
229232
if err = eg.Wait(); err != nil {
230233
return artifact.Reference{}, xerrors.Errorf("analyze error: %w", err)
231234
}
235+
if walkErr != nil {
236+
return artifact.Reference{}, walkErr
237+
}
232238

233239
// Post-analysis
234240
if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil {

pkg/fanal/artifact/local/fs_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package local
22

33
import (
4+
"context"
5+
"fmt"
46
"os"
57
"path/filepath"
8+
"strings"
69
"testing"
710

811
"github.com/stretchr/testify/assert"
@@ -15,6 +18,7 @@ import (
1518
"github.com/aquasecurity/trivy/pkg/fanal/types"
1619
"github.com/aquasecurity/trivy/pkg/fanal/walker"
1720
"github.com/aquasecurity/trivy/pkg/misconf"
21+
trivytypes "github.com/aquasecurity/trivy/pkg/types"
1822
"github.com/aquasecurity/trivy/pkg/uuid"
1923

2024
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all"
@@ -2663,3 +2667,40 @@ func Test_sanitizeRemoteURL(t *testing.T) {
26632667
})
26642668
}
26652669
}
2670+
2671+
// userErrorAnalyzer fails every matching file with a *types.UserError,
2672+
// emulating a fatal analyzer error such as a remote Maven 429.
2673+
type userErrorAnalyzer struct{}
2674+
2675+
func (userErrorAnalyzer) Type() analyzer.Type { return "user-error-test" }
2676+
func (userErrorAnalyzer) Version() int { return 1 }
2677+
func (userErrorAnalyzer) Required(filePath string, _ os.FileInfo) bool {
2678+
return strings.HasSuffix(filePath, ".usererror")
2679+
}
2680+
2681+
func (userErrorAnalyzer) Analyze(_ context.Context, _ analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
2682+
return nil, &trivytypes.UserError{Message: "429 Too Many Requests"}
2683+
}
2684+
2685+
// TestArtifact_Inspect_AnalyzeErrorNotMasked is a regression test for #10790:
2686+
// a fatal analyzer error (a remote Maven 429, as *types.UserError) must be
2687+
// surfaced, not masked by the context.Canceled the walk hits after egCtx cancels.
2688+
func TestArtifact_Inspect_AnalyzeErrorNotMasked(t *testing.T) {
2689+
dir := t.TempDir()
2690+
// Parallel=1 with several files: the first file cancels egCtx, so a later
2691+
// file's semaphore acquire fails with context.Canceled.
2692+
for i := range 10 {
2693+
require.NoError(t, os.WriteFile(
2694+
filepath.Join(dir, fmt.Sprintf("%d.usererror", i)), []byte("x"), 0o600))
2695+
}
2696+
2697+
analyzer.RegisterAnalyzer(userErrorAnalyzer{})
2698+
t.Cleanup(func() { analyzer.DeregisterAnalyzer("user-error-test") })
2699+
2700+
a, err := NewArtifact(dir, cache.NewMemoryCache(), walker.NewFS(), artifact.Option{Parallel: 1})
2701+
require.NoError(t, err)
2702+
2703+
_, err = a.Inspect(t.Context())
2704+
require.Error(t, err)
2705+
assert.ErrorContains(t, err, "429 Too Many Requests")
2706+
}

pkg/fanal/artifact/vm/vm.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ func (a *Storage) Analyze(ctx context.Context, r *io.SectionReader) (types.BlobI
102102
defer composite.Cleanup()
103103

104104
// TODO: Always walk from the root directory. Consider whether there is a need to be able to set optional
105-
err = a.walker.Walk(r, "/", a.artifactOption.WalkerOption, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
105+
walkErr := a.walker.Walk(r, "/", a.artifactOption.WalkerOption, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
106106
path := strings.TrimPrefix(filePath, "/")
107-
if err = a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "/", path, info, opener, nil, opts); err != nil {
107+
if err := a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "/", path, info, opener, nil, opts); err != nil {
108108
return xerrors.Errorf("analyze file (%s): %w", path, err)
109109
}
110110

@@ -126,14 +126,16 @@ func (a *Storage) Analyze(ctx context.Context, r *io.SectionReader) (types.BlobI
126126

127127
return nil
128128
})
129-
if err != nil {
130-
return types.BlobInfo{}, xerrors.Errorf("walk vm error: %w", err)
131-
}
132129

133-
// Wait for all the goroutine to finish.
130+
// errgroup cancels egCtx when an analysis goroutine fails, so the walk above can
131+
// fail with context.Canceled and mask the real cause (e.g. a remote 429).
132+
// Surface eg.Wait()'s error first; fall back to the walk error only when the group is clean.
134133
if err = eg.Wait(); err != nil {
135134
return types.BlobInfo{}, xerrors.Errorf("analyze error: %w", err)
136135
}
136+
if walkErr != nil {
137+
return types.BlobInfo{}, xerrors.Errorf("walk vm error: %w", walkErr)
138+
}
137139

138140
// Post-analysis
139141
if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil {

0 commit comments

Comments
 (0)