Following up on GHSA-2f6x-xr4j-f2h7 — @another-rex asked me to convert it into an issue, and having
looked at it again I think he was right that it isn't a vulnerability. Reframing it here as what I
now believe it actually is: an exit-code contract violation.
What happens
extractRlibArchive calls log.Fatalf on any error from the archive iterator, including the
ordinary io.EOF that ends every ar archive:
|
for { |
|
header, err := reader.Next() |
|
if err != nil { |
|
log.Fatalf("%v", err) |
|
} |
|
if header.Name == "//" { // "//" is used in GNU ar format as a store for long file names |
for {
header, err := reader.Next()
if err != nil {
log.Fatalf("%v", err) // os.Exit(1) — including on io.EOF
}
The loop's only exit is finding a /0 member or one ending in .rcgu.o/. An ar archive with
neither walks to end-of-archive, ar.Reader.Next() returns io.EOF, and the process exits.
No malformed or hostile input is needed — a well-formed ar file that simply isn't a Rust rlib is
enough. I reproduced it with ar rc /tmp/plain.a /tmp/a.txt and the loop above.
Why I think this is worth fixing — the exit code is wrong
docs/output.md defines the contract:
| Exit code |
Reason |
0 |
Packages were found when scanning, but does not match any known vulnerabilities or findings |
1 |
Packages were found when scanning, and there are vulnerabilities or findings |
127 |
General Error |
129-255 |
Reserved for non result related errors |
log.Fatalf exits 1. So an internal failure is reported to the caller as "there are
vulnerabilities or findings" — a result-related code — when it should be 127. A CI job can't
distinguish "this scan found problems" from "this scan died on an unrelated build artifact", and
the scan produces no output either way.
To be clear about what this is not: the process exits non-zero, so CI does go red. Nothing is
silently hidden. That's precisely why I no longer think it's a security issue — but the outcome is
still being misreported through the documented interface.
The caller already handles this correctly
rust.go:53-57 is written to survive exactly this:
buf, err := extractRlibArchive(path)
if err != nil {
cmdlogger.Errorf("failed to analyse '%s': %s", path, err)
continue
}
log.Fatalf exits before that error can be returned, so the recovery never runs. One bad artifact
ends the whole scan instead of being skipped.
This is the only log.Fatalf in non-test library code in the repository.
Suggested fix
Return the error and treat end-of-archive as a normal termination:
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return bytes.Buffer{}, fmt.Errorf("no object file found in rlib archive '%s'", rlibPath)
}
if err != nil {
return bytes.Buffer{}, fmt.Errorf("error reading rlib archive '%s': %w", rlibPath, err)
}
The existing continue then does what it was written to do: skip that artifact and carry on. The
exit code also stops being wrong, because the scan completes normally.
Reached via --experimental-call-analysis on a Rust project with a .rlib in its build output.
Happy to send a PR with a regression test if that's useful — @another-rex offered, so I'll open one
shortly and you can close it if you'd rather take a different approach.
Found against 5bc4ef461c3e57ae246d80a4c956a5245390bca8. I used an AI assistant while
investigating; I read the code paths and ran the reproduction myself.
Following up on GHSA-2f6x-xr4j-f2h7 — @another-rex asked me to convert it into an issue, and having
looked at it again I think he was right that it isn't a vulnerability. Reframing it here as what I
now believe it actually is: an exit-code contract violation.
What happens
extractRlibArchivecallslog.Fatalfon any error from the archive iterator, including theordinary
io.EOFthat ends every ar archive:osv-scanner/internal/sourceanalysis/rust.go
Lines 194 to 199 in 5bc4ef4
The loop's only exit is finding a
/0member or one ending in.rcgu.o/. An ar archive withneither walks to end-of-archive,
ar.Reader.Next()returnsio.EOF, and the process exits.No malformed or hostile input is needed — a well-formed ar file that simply isn't a Rust rlib is
enough. I reproduced it with
ar rc /tmp/plain.a /tmp/a.txtand the loop above.Why I think this is worth fixing — the exit code is wrong
docs/output.mddefines the contract:01127129-255log.Fatalfexits 1. So an internal failure is reported to the caller as "there arevulnerabilities or findings" — a result-related code — when it should be
127. A CI job can'tdistinguish "this scan found problems" from "this scan died on an unrelated build artifact", and
the scan produces no output either way.
To be clear about what this is not: the process exits non-zero, so CI does go red. Nothing is
silently hidden. That's precisely why I no longer think it's a security issue — but the outcome is
still being misreported through the documented interface.
The caller already handles this correctly
rust.go:53-57is written to survive exactly this:log.Fatalfexits before that error can be returned, so the recovery never runs. One bad artifactends the whole scan instead of being skipped.
This is the only
log.Fatalfin non-test library code in the repository.Suggested fix
Return the error and treat end-of-archive as a normal termination:
The existing
continuethen does what it was written to do: skip that artifact and carry on. Theexit code also stops being wrong, because the scan completes normally.
Reached via
--experimental-call-analysison a Rust project with a.rlibin its build output.Happy to send a PR with a regression test if that's useful — @another-rex offered, so I'll open one
shortly and you can close it if you'd rather take a different approach.
Found against
5bc4ef461c3e57ae246d80a4c956a5245390bca8. I used an AI assistant whileinvestigating; I read the code paths and ran the reproduction myself.