I found a few robustness issues where malformed coverage artifacts or path data can make grcov panic during parsing/report generation.
The main cases below are reachable through normal coverage-processing inputs: LCOV text, GCNO binary data, and filesystem paths discovered by the producer/HTML pipeline. A single bad artifact can abort the current report run instead of being handled as a parse/output error.
I checked the nearby comments around the panic sites below. The comments describe record formats or output behavior, but I did not find comments or docs that cover these panic preconditions.
Version checked: grcov 0.10.7
Main examples
| Area |
Panic site |
Trigger |
| LCOV parser |
src/parser.rs:259, src/parser.rs:275, src/parser.rs:281, src/parser.rs:345, src/parser.rs:385, src/parser.rs:395, src/parser.rs:405 |
Oversized numeric fields or duplicate counters overflow during manual decimal parsing/aggregation. |
| LCOV parser |
src/parser.rs:194, src/parser.rs:646 |
end_of_record or final lines before any SF: record make cur_file.unwrap() panic. |
| GCNO parser |
src/reader.rs:245 |
A non-empty all-zero string field makes position(...).unwrap() panic. |
| Path handling |
src/producer.rs:33, src/producer.rs:68, src/html.rs:206, src/html.rs:428 |
Non-UTF-8 filenames/extensions or malformed relative paths are unwrapped during producer/HTML output. |
Example 1: LCOV numeric overflow
Relevant code:
let line_no = iter
.take_while(|&&c| c.is_ascii_digit())
.fold(0, |r, &x| r * 10 + u32::from(x - b'0'));
let execution_count = iter
.take_while(|&&c| c.is_ascii_digit())
.fold(u64::from(*c - b'0'), |r, &x| {
r * 10 + u64::from(x - b'0')
});
*cur_lines.entry(line_no).or_insert(0) += execution_count;
The parser already has a ParserError path for malformed records, but very large decimal fields can overflow before that path is used.
Minimal reproducer shape:
use grcov::parse_lcov;
#[test]
#[should_panic]
fn lcov_da_counter_overflow_panics() {
let input = b"SF:/tmp/overflow.rs\nDA:7,18446744073709551615\nDA:7,1\nend_of_record\n".to_vec();
let _ = parse_lcov(input, false, false);
}
Other LCOV fields use the same unchecked decimal-fold pattern, including FNDA and BRDA.
Expected behavior:
Malformed or oversized LCOV numbers should return ParserError or be ignored when ignore_parsing_error is enabled.
Actual behavior:
Debug/test builds can panic on integer overflow. Release builds may silently wrap and continue with incorrect coverage data.
Example 2: LCOV record before SF: panics
Relevant code:
// we've a end_of_record
results.push((
cur_file.unwrap(),
CovResult {
lines: cur_lines,
branches: cur_branches,
functions: cur_functions,
},
));
Minimal reproducer shape:
use grcov::parse_lcov;
#[test]
#[should_panic]
fn lcov_end_record_without_source_file_panics() {
let input = b"end_of_record\n".to_vec();
let _ = parse_lcov(input, false, false);
}
Expected behavior:
An LCOV record without a source file should be rejected as malformed input.
Actual behavior:
The parser unwraps the missing source file and panics.
Example 3: GCNO all-zero string field
Relevant code:
let bytes = &self.buffer[start..self.pos];
let i = len - bytes.iter().rev().position(|&x| x != 0).unwrap();
Ok(unsafe { std::str::from_utf8_unchecked(&bytes[..i]).to_string() })
If the GCNO string field has a non-zero word length but all bytes are zero, position(|&x| x != 0) returns None.
Minimal reproducer shape:
use grcov::Gcno;
#[test]
fn gcno_all_zero_string_field_panics() {
let mut gcno = Vec::new();
gcno.extend_from_slice(b"oncg"); // little-endian gcno marker
gcno.extend_from_slice(b"*09A"); // version that reaches read_string
gcno.extend_from_slice(&0u32.to_le_bytes()); // checksum
gcno.extend_from_slice(&1u32.to_le_bytes()); // string length: one u32 word
gcno.extend_from_slice(&[0, 0, 0, 0]); // all-zero string payload
let result = std::panic::catch_unwind(|| {
let _ = Gcno::compute("bad.gcno", gcno, vec![], false);
});
assert!(result.is_err());
}
Expected behavior:
The malformed GCNO string should produce a structured parse error.
Actual behavior:
The parser panics on unwrap().
Example 4: non-UTF-8 filenames during producer/HTML handling
Relevant code:
fn clean_path(path: &Path) -> String {
path.to_str().unwrap().to_string()
}
if let Some(ext) = path.extension() {
match ext.to_str().unwrap() {
"gcno" => { /* ... */ }
"gcda" => { /* ... */ }
"info" => { /* ... */ }
_ => {}
}
}
HTML output has the same pattern when building output names:
let mut ext = ext.to_str().unwrap().to_owned();
let filename = rel_path.file_name().unwrap().to_str().unwrap();
Minimal reproducer shape on Unix:
use crossbeam_channel::unbounded;
use grcov::producer;
use std::ffi::OsString;
use std::fs;
use std::os::unix::ffi::OsStringExt;
#[test]
#[should_panic]
fn non_utf8_info_filename_panics_in_producer() {
let base = tempfile::tempdir().unwrap();
let mut bad_name = vec![b'f', b'o', 0x80, b'o'];
bad_name.extend_from_slice(b".info");
fs::write(base.path().join(OsString::from_vec(bad_name)), b"TN:\n").unwrap();
let (sender, _receiver) = unbounded();
let inputs = vec![base.path().to_string_lossy().into_owned()];
let _ = producer(base.path(), &inputs, &sender, false, false);
}
Expected behavior:
The producer/report pipeline should skip the malformed path, preserve it as an OsStr, or return/log a normal error.
Actual behavior:
The path is converted with to_str().unwrap() and the report run panics.
Suggested fix direction
- Use checked decimal parsing for LCOV numeric fields, and route failures through
ParserError.
- Treat missing
SF: as a malformed LCOV record instead of unwrapping cur_file.
- In GCNO string parsing, handle all-zero payloads without unwrapping
None.
- Avoid
Path::to_str().unwrap() for discovered files; either use lossy display only for messages or propagate a recoverable path error.
Additional lower-priority observations
Coverage-stat aggregation helpers
There are several public or semi-public aggregation helpers that assume valid internal counts:
pub fn new(total: usize, covered: usize, precision: usize) -> Self {
let missed = total - covered;
/* ... */
}
pub fn add(&mut self, other: &Self) {
self.total += other.total;
self.covered += other.covered;
self.missed += other.missed;
}
if let Some(line) = lines.get_mut((*line_num - 1) as usize) {
/* ... */
}
These can panic for inputs such as covered > total, very large accumulated counters, extreme precision, or line number 0. I would mention them as hardening opportunities, but I would not lead with them unless the maintainers consider those constructors part of the supported public API.
GCOV version/tool handling
get_gcov_version() and parse_version() assume gcov --version succeeds, emits UTF-8, and contains a parseable semantic version:
let output = Command::new(get_gcov())
.arg("--version")
.output()
.expect("Failed to execute `gcov`. `gcov` is required (it is part of GCC).");
assert!(output.status.success(), "`gcov` failed to execute.");
let output = String::from_utf8(output.stdout).unwrap();
assert!(version.is_some(), "no version found for `gcov`.");
This is probably best framed as CLI/tooling robustness, not as a core parser bug.
Template, channel, mutex, and writer failures
Generated tests also found panics around missing templates, closed work channels, poisoned mutexes, and output writer failures. Those are real panic paths, but they are weaker as issue lead examples because they depend more on internal workflow state or injected failing components.
I found a few robustness issues where malformed coverage artifacts or path data can make
grcovpanic during parsing/report generation.The main cases below are reachable through normal coverage-processing inputs: LCOV text, GCNO binary data, and filesystem paths discovered by the producer/HTML pipeline. A single bad artifact can abort the current report run instead of being handled as a parse/output error.
I checked the nearby comments around the panic sites below. The comments describe record formats or output behavior, but I did not find comments or docs that cover these panic preconditions.
Version checked:
grcov 0.10.7Main examples
src/parser.rs:259,src/parser.rs:275,src/parser.rs:281,src/parser.rs:345,src/parser.rs:385,src/parser.rs:395,src/parser.rs:405src/parser.rs:194,src/parser.rs:646end_of_recordor final lines before anySF:record makecur_file.unwrap()panic.src/reader.rs:245position(...).unwrap()panic.src/producer.rs:33,src/producer.rs:68,src/html.rs:206,src/html.rs:428Example 1: LCOV numeric overflow
Relevant code:
The parser already has a
ParserErrorpath for malformed records, but very large decimal fields can overflow before that path is used.Minimal reproducer shape:
Other LCOV fields use the same unchecked decimal-fold pattern, including
FNDAandBRDA.Expected behavior:
Malformed or oversized LCOV numbers should return
ParserErroror be ignored whenignore_parsing_erroris enabled.Actual behavior:
Debug/test builds can panic on integer overflow. Release builds may silently wrap and continue with incorrect coverage data.
Example 2: LCOV record before
SF:panicsRelevant code:
Minimal reproducer shape:
Expected behavior:
An LCOV record without a source file should be rejected as malformed input.
Actual behavior:
The parser unwraps the missing source file and panics.
Example 3: GCNO all-zero string field
Relevant code:
If the GCNO string field has a non-zero word length but all bytes are zero,
position(|&x| x != 0)returnsNone.Minimal reproducer shape:
Expected behavior:
The malformed GCNO string should produce a structured parse error.
Actual behavior:
The parser panics on
unwrap().Example 4: non-UTF-8 filenames during producer/HTML handling
Relevant code:
HTML output has the same pattern when building output names:
Minimal reproducer shape on Unix:
Expected behavior:
The producer/report pipeline should skip the malformed path, preserve it as an
OsStr, or return/log a normal error.Actual behavior:
The path is converted with
to_str().unwrap()and the report run panics.Suggested fix direction
ParserError.SF:as a malformed LCOV record instead of unwrappingcur_file.None.Path::to_str().unwrap()for discovered files; either use lossy display only for messages or propagate a recoverable path error.Additional lower-priority observations
Coverage-stat aggregation helpers
There are several public or semi-public aggregation helpers that assume valid internal counts:
These can panic for inputs such as
covered > total, very large accumulated counters, extreme precision, or line number0. I would mention them as hardening opportunities, but I would not lead with them unless the maintainers consider those constructors part of the supported public API.GCOV version/tool handling
get_gcov_version()andparse_version()assumegcov --versionsucceeds, emits UTF-8, and contains a parseable semantic version:This is probably best framed as CLI/tooling robustness, not as a core parser bug.
Template, channel, mutex, and writer failures
Generated tests also found panics around missing templates, closed work channels, poisoned mutexes, and output writer failures. Those are real panic paths, but they are weaker as issue lead examples because they depend more on internal workflow state or injected failing components.