From 34a169024a8879fab2232a0143314eaf76d94e86 Mon Sep 17 00:00:00 2001 From: Kobi Hikri Date: Mon, 10 Aug 2026 11:53:14 +0300 Subject: [PATCH 1/2] fix(sourceanalysis): return an error instead of exiting when an rlib has no object file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractRlibArchive called log.Fatalf on any error from the archive iterator, including the io.EOF that ends every ar archive. An archive with no "/0" member and no ".rcgu.o/" member walks the loop to end-of-archive and exits the process. The caller at rust.go:53-57 already logs the error and continues to the next artifact, but log.Fatalf exits before the error can be returned, so one unrelated build artifact ends the whole scan. It also reports the wrong exit code. log.Fatalf exits 1, which docs/output.md reserves for "packages were found when scanning, and there are vulnerabilities or findings" — a result-related code. An internal failure should be 127. Treat end-of-archive as "this is not an rlib we can analyse" and return an error, letting the existing continue skip the artifact and the scan finish normally. This was the only log.Fatalf in non-test library code. Fixes #2976 --- internal/sourceanalysis/rust.go | 9 ++++++-- internal/sourceanalysis/rust_test.go | 34 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/internal/sourceanalysis/rust.go b/internal/sourceanalysis/rust.go index b6a1c8720db..4e016148a6b 100644 --- a/internal/sourceanalysis/rust.go +++ b/internal/sourceanalysis/rust.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "log" "os" "os/exec" "path/filepath" @@ -193,8 +192,14 @@ func extractRlibArchive(rlibPath string) (bytes.Buffer, error) { } for { header, err := reader.Next() + // Reaching the end of the archive without finding an object file means this is not an + // rlib we can analyse. Return an error so the caller can skip this artifact, rather than + // ending the process and with it the rest of the scan. + if errors.Is(err, io.EOF) { + return bytes.Buffer{}, fmt.Errorf("no object file found in rlib archive '%s'", rlibPath) + } if err != nil { - log.Fatalf("%v", err) + return bytes.Buffer{}, fmt.Errorf("failed to read rlib archive '%s': %w", rlibPath, err) } if header.Name == "//" { // "//" is used in GNU ar format as a store for long file names fileBuf := bytes.Buffer{} diff --git a/internal/sourceanalysis/rust_test.go b/internal/sourceanalysis/rust_test.go index 9d7db5f281a..75a62a75217 100644 --- a/internal/sourceanalysis/rust_test.go +++ b/internal/sourceanalysis/rust_test.go @@ -2,6 +2,7 @@ package sourceanalysis import ( "bytes" + "fmt" "os" "path/filepath" "reflect" @@ -103,3 +104,36 @@ func Test_rustBuildSource(t *testing.T) { } } } + +// A well-formed ar archive that is not a Rust rlib — it contains no "/0" member and no +// ".rcgu.o/" member — must be reported as an error the caller can skip, not end the process. +// Before this was fixed, the iteration loop reached end-of-archive and called log.Fatalf on the +// resulting io.EOF, which is os.Exit(1). That killed the whole scan on one unrelated build +// artifact, and reported the failure through the exit code reserved for "vulnerabilities or +// findings" (see docs/output.md). +func Test_extractRlibArchive_noObjectFile(t *testing.T) { + t.Parallel() + + // A minimal GNU ar archive with a single plain member, built by hand so the test needs no + // `ar` binary on the machine running it. + var archive bytes.Buffer + archive.WriteString("!\n") + const content = "hi" + // name[16] mtime[12] uid[6] gid[6] mode[8] size[10] fmag[2] + fmt.Fprintf(&archive, "%-16s%-12d%-6d%-6d%-8s%-10d`\n", "a.txt/", 0, 0, 0, "100644", len(content)) + archive.WriteString(content) + + path := filepath.Join(t.TempDir(), "not-an-rlib.rlib") + if err := os.WriteFile(path, archive.Bytes(), 0600); err != nil { + t.Fatalf("failed to write test archive: %v", err) + } + + // If the regression returns, this call terminates the test binary instead of failing it. + _, err := extractRlibArchive(path) + if err == nil { + t.Fatal("expected an error for an ar archive with no object file, got nil") + } + if !strings.Contains(err.Error(), "no object file found") { + t.Errorf("expected a 'no object file found' error, got: %v", err) + } +} From ed37301a39e2a3669da5ef412fa5d1fe8b70c58d Mon Sep 17 00:00:00 2001 From: Kobi Hikri Date: Tue, 11 Aug 2026 09:29:50 +0300 Subject: [PATCH 2/2] fix(sourceanalysis): scale back comments per review Drop the comments describing the previous log.Fatal behaviour and the one framing the test as guarding a specific fix; the git history carries that. Code and test logic are unchanged. Signed-off-by: Kobi Hikri --- internal/sourceanalysis/rust.go | 3 --- internal/sourceanalysis/rust_test.go | 12 +++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/internal/sourceanalysis/rust.go b/internal/sourceanalysis/rust.go index 4e016148a6b..f7601e15388 100644 --- a/internal/sourceanalysis/rust.go +++ b/internal/sourceanalysis/rust.go @@ -192,9 +192,6 @@ func extractRlibArchive(rlibPath string) (bytes.Buffer, error) { } for { header, err := reader.Next() - // Reaching the end of the archive without finding an object file means this is not an - // rlib we can analyse. Return an error so the caller can skip this artifact, rather than - // ending the process and with it the rest of the scan. if errors.Is(err, io.EOF) { return bytes.Buffer{}, fmt.Errorf("no object file found in rlib archive '%s'", rlibPath) } diff --git a/internal/sourceanalysis/rust_test.go b/internal/sourceanalysis/rust_test.go index 75a62a75217..1dca97c477b 100644 --- a/internal/sourceanalysis/rust_test.go +++ b/internal/sourceanalysis/rust_test.go @@ -105,17 +105,12 @@ func Test_rustBuildSource(t *testing.T) { } } -// A well-formed ar archive that is not a Rust rlib — it contains no "/0" member and no -// ".rcgu.o/" member — must be reported as an error the caller can skip, not end the process. -// Before this was fixed, the iteration loop reached end-of-archive and called log.Fatalf on the -// resulting io.EOF, which is os.Exit(1). That killed the whole scan on one unrelated build -// artifact, and reported the failure through the exit code reserved for "vulnerabilities or -// findings" (see docs/output.md). +// An ar archive with no object file member is not an rlib we can analyse, so it should return +// an error. func Test_extractRlibArchive_noObjectFile(t *testing.T) { t.Parallel() - // A minimal GNU ar archive with a single plain member, built by hand so the test needs no - // `ar` binary on the machine running it. + // Built by hand so the test needs no `ar` binary on the machine running it. var archive bytes.Buffer archive.WriteString("!\n") const content = "hi" @@ -128,7 +123,6 @@ func Test_extractRlibArchive_noObjectFile(t *testing.T) { t.Fatalf("failed to write test archive: %v", err) } - // If the regression returns, this call terminates the test binary instead of failing it. _, err := extractRlibArchive(path) if err == nil { t.Fatal("expected an error for an ar archive with no object file, got nil")