Skip to content

grcov can panic on several malformed coverage inputs instead of reporting parse/output errors #1511

Description

@xc01

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions