From 87ea190459a4a52b83f7c1de2f6852bdcabcba3a Mon Sep 17 00:00:00 2001 From: Yuval Kashtan Date: Wed, 1 Apr 2026 16:48:09 +0000 Subject: [PATCH 1/2] fix(dpkg): handle missing files in distroless status.d gracefully The distroless scanner failed the entire scan when a file in status.d/ could not be opened (e.g., a dangling symlink to a .list file in info/). Now skips missing files with a warning instead of aborting. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Yuval Kashtan --- dpkg/distroless_scanner.go | 4 ++ dpkg/distroless_scanner_test.go | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/dpkg/distroless_scanner.go b/dpkg/distroless_scanner.go index 42bbb8109..2082070f1 100644 --- a/dpkg/distroless_scanner.go +++ b/dpkg/distroless_scanner.go @@ -81,6 +81,10 @@ func (ps *DistrolessScanner) Scan(ctx context.Context, layer *claircore.Layer) ( log.DebugContext(ctx, "examining package database") db, err := sys.Open(fn) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + log.WarnContext(ctx, "skipping missing database file", "reason", err) + continue + } return fmt.Errorf("reading database files from layer failed: %w", err) } diff --git a/dpkg/distroless_scanner_test.go b/dpkg/distroless_scanner_test.go index 45533c2a0..878df5a95 100644 --- a/dpkg/distroless_scanner_test.go +++ b/dpkg/distroless_scanner_test.go @@ -1,7 +1,14 @@ package dpkg import ( + "archive/tar" + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" "testing" + "time" "github.com/google/go-cmp/cmp" @@ -61,3 +68,79 @@ func TestDistrolessLayer(t *testing.T) { t.Fatal(cmp.Diff(ps, want)) } } + +func TestDistrolessMissingListFile(t *testing.T) { + ctx := context.Background() + now := time.Now() + + // Build a tar with: + // - var/lib/dpkg/status.d/gcc-14-base (valid control file) + // - var/lib/dpkg/status.d/gcc-14-base.list (symlink to missing ../info/gcc-14-base.list) + controlData := []byte("Package: gcc-14-base\nVersion: 14.2.0-19\nArchitecture: amd64\nSource: gcc-14\n\n") + buf := &bytes.Buffer{} + h := sha256.New() + tw := tar.NewWriter(io.MultiWriter(buf, h)) + + for _, dir := range []string{ + "var/lib/dpkg/", + "var/lib/dpkg/status.d/", + } { + if err := tw.WriteHeader(&tar.Header{ + Typeflag: tar.TypeDir, + Name: dir, + Mode: 0755, + ModTime: now, + }); err != nil { + t.Fatal(err) + } + } + if err := tw.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "var/lib/dpkg/status.d/gcc-14-base", + Size: int64(len(controlData)), + Mode: 0644, + ModTime: now, + }); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(controlData); err != nil { + t.Fatal(err) + } + if err := tw.WriteHeader(&tar.Header{ + Typeflag: tar.TypeSymlink, + Name: "var/lib/dpkg/status.d/gcc-14-base.list", + Linkname: "../info/gcc-14-base.list", + Mode: 0644, + ModTime: now, + }); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + desc := claircore.LayerDescription{ + URI: "file:///dev/null", + Digest: fmt.Sprintf("sha256:%x", h.Sum(nil)), + MediaType: "application/vnd.oci.image.layer.v1.tar", + } + var l claircore.Layer + if err := l.Init(ctx, &desc, bytes.NewReader(buf.Bytes())); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { l.Close() }) + + var s DistrolessScanner + ps, err := s.Scan(ctx, &l) + if err != nil { + t.Fatalf("scan should not fail with missing .list file: %v", err) + } + + // Should still find the valid package. + if got := len(ps); got != 1 { + t.Fatalf("got %d packages, want 1", got) + } + if ps[0].Name != "gcc-14-base" { + t.Errorf("got package name %q, want %q", ps[0].Name, "gcc-14-base") + } +} From 0ba29c7653ba203ea64c5ded92099f0c76d8a210 Mon Sep 17 00:00:00 2001 From: Yuval Kashtan Date: Fri, 24 Apr 2026 16:38:47 +0000 Subject: [PATCH 2/2] fix(dpkg): address PR review feedback on test Use test.Logging, test.GenerateFixture, and test.AnyDescription instead of hand-rolled context, sha256 computation, and in-memory tar buffer. Co-Authored-By: Claude Opus 4.6 (1M context) --- dpkg/distroless_scanner_test.go | 105 ++++++++++++++------------------ 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/dpkg/distroless_scanner_test.go b/dpkg/distroless_scanner_test.go index 878df5a95..4b89a9509 100644 --- a/dpkg/distroless_scanner_test.go +++ b/dpkg/distroless_scanner_test.go @@ -2,13 +2,8 @@ package dpkg import ( "archive/tar" - "bytes" - "context" - "crypto/sha256" - "fmt" - "io" + "os" "testing" - "time" "github.com/google/go-cmp/cmp" @@ -70,77 +65,71 @@ func TestDistrolessLayer(t *testing.T) { } func TestDistrolessMissingListFile(t *testing.T) { - ctx := context.Background() - now := time.Now() + t.Parallel() + mod := test.Modtime(t, "distroless_scanner_test.go") + layerfile := test.GenerateFixture(t, "distroless-missing-list.tar", mod, missingListSetup) + ctx := test.Logging(t) + var l claircore.Layer + var s DistrolessScanner + + f, err := os.Open(layerfile) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if err := l.Init(ctx, &test.AnyDescription, f); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := l.Close(); err != nil { + t.Error(err) + } + }) - // Build a tar with: - // - var/lib/dpkg/status.d/gcc-14-base (valid control file) - // - var/lib/dpkg/status.d/gcc-14-base.list (symlink to missing ../info/gcc-14-base.list) - controlData := []byte("Package: gcc-14-base\nVersion: 14.2.0-19\nArchitecture: amd64\nSource: gcc-14\n\n") - buf := &bytes.Buffer{} - h := sha256.New() - tw := tar.NewWriter(io.MultiWriter(buf, h)) + ps, err := s.Scan(ctx, &l) + if err != nil { + t.Fatalf("scan should not fail with missing .list file: %v", err) + } + if got := len(ps); got != 1 { + t.Fatalf("got %d packages, want 1", got) + } + if ps[0].Name != "gcc-14-base" { + t.Errorf("got package name %q, want %q", ps[0].Name, "gcc-14-base") + } +} +func missingListSetup(t testing.TB, f *os.File) { + w := tar.NewWriter(f) + defer func() { + if err := w.Close(); err != nil { + t.Error(err) + } + }() for _, dir := range []string{ "var/lib/dpkg/", "var/lib/dpkg/status.d/", } { - if err := tw.WriteHeader(&tar.Header{ - Typeflag: tar.TypeDir, - Name: dir, - Mode: 0755, - ModTime: now, + if err := w.WriteHeader(&tar.Header{ + Name: dir, }); err != nil { t.Fatal(err) } } - if err := tw.WriteHeader(&tar.Header{ - Typeflag: tar.TypeReg, - Name: "var/lib/dpkg/status.d/gcc-14-base", - Size: int64(len(controlData)), - Mode: 0644, - ModTime: now, + const controlData = "Package: gcc-14-base\nVersion: 14.2.0-19\nArchitecture: amd64\nSource: gcc-14\n\n" + if err := w.WriteHeader(&tar.Header{ + Name: "var/lib/dpkg/status.d/gcc-14-base", + Size: int64(len(controlData)), }); err != nil { t.Fatal(err) } - if _, err := tw.Write(controlData); err != nil { + if _, err := w.Write([]byte(controlData)); err != nil { t.Fatal(err) } - if err := tw.WriteHeader(&tar.Header{ + if err := w.WriteHeader(&tar.Header{ Typeflag: tar.TypeSymlink, Name: "var/lib/dpkg/status.d/gcc-14-base.list", Linkname: "../info/gcc-14-base.list", - Mode: 0644, - ModTime: now, }); err != nil { t.Fatal(err) } - if err := tw.Close(); err != nil { - t.Fatal(err) - } - - desc := claircore.LayerDescription{ - URI: "file:///dev/null", - Digest: fmt.Sprintf("sha256:%x", h.Sum(nil)), - MediaType: "application/vnd.oci.image.layer.v1.tar", - } - var l claircore.Layer - if err := l.Init(ctx, &desc, bytes.NewReader(buf.Bytes())); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { l.Close() }) - - var s DistrolessScanner - ps, err := s.Scan(ctx, &l) - if err != nil { - t.Fatalf("scan should not fail with missing .list file: %v", err) - } - - // Should still find the valid package. - if got := len(ps); got != 1 { - t.Fatalf("got %d packages, want 1", got) - } - if ps[0].Name != "gcc-14-base" { - t.Errorf("got package name %q, want %q", ps[0].Name, "gcc-14-base") - } }