Skip to content

feat(coverage): add LLVM source-based coverage backend - #8

Open
zjy-dev wants to merge 1 commit into
mainfrom
001-llvm-coverage-engine
Open

zjy-dev wants to merge 1 commit into
mainfrom
001-llvm-coverage-engine

Conversation

@zjy-dev

@zjy-dev zjy-dev commented Jun 11, 2026 •

Copy link
Copy Markdown
Owner

Implement LLVM/Clang coverage backend as an alternative to the existing GCC gcov/gcovr implementation. The new backend uses:

  • -fprofile-instr-generate -fcoverage-mapping for instrumented builds
  • llvm-profdata merge + llvm-cov export for JSON coverage reports
  • LLVM IR parsing (.ll files) for CFG-guided basic block selection

Key changes:

  • New LLVMCoverage type implementing Coverage + FilteredLineExtractor
  • New LLVMReport with canonical on-disk line-set format
  • New LLVM IR CFG parser for function/BB/successor/line extraction
  • New FilteredLineExtractor interface to decouple engine from backend type
  • Config coverage_backend field ("gcc" default, "llvm" optional)
  • NewAnalyzerFromCFGFunctions to reuse existing BB selection logic
  • Full test suite including end-to-end integration tests

Summary by Sourcery

Add an LLVM-based coverage backend alongside the existing GCC/gcov implementation and integrate it into the fuzzing pipeline.

New Features:

  • Introduce an LLVMCoverage implementation with support for llvm-profdata/llvm-cov reporting and filtered covered line extraction.
  • Add LLVM IR-based CFG parsing and analyzer construction for target function selection in fuzzing.
  • Support selecting coverage backends via configuration, including a sample clang-based LLVM coverage config.

Enhancements:

  • Refactor fuzzing engine coverage handling to work against a generic FilteredLineExtractor interface instead of a GCC-specific type.
  • Factor GCC CFG analyzer construction into a helper for reuse and clearer separation between GCC and LLVM backends.

Tests:

  • Add unit and integration tests for the LLVM coverage backend, LLVM IR CFG parser, and llvm-cov report handling.
  • Extend config validation tests to cover the new coverage_backend options and requirements.

Implement LLVM/Clang coverage backend as an alternative to the existing
GCC gcov/gcovr implementation. The new backend uses:
- `-fprofile-instr-generate -fcoverage-mapping` for instrumented builds
- `llvm-profdata merge` + `llvm-cov export` for JSON coverage reports
- LLVM IR parsing (`.ll` files) for CFG-guided basic block selection

Key changes:
- New `LLVMCoverage` type implementing `Coverage` + `FilteredLineExtractor`
- New `LLVMReport` with canonical on-disk line-set format
- New LLVM IR CFG parser for function/BB/successor/line extraction
- New `FilteredLineExtractor` interface to decouple engine from backend type
- Config `coverage_backend` field (`"gcc"` default, `"llvm"` optional)
- `NewAnalyzerFromCFGFunctions` to reuse existing BB selection logic
- Full test suite including end-to-end integration tests
@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds an LLVM/Clang source-based coverage backend alongside the existing GCC/gcovr backend, wires it into the fuzzing app and engine via a backend-agnostic Coverage/FilteredLineExtractor interface, and introduces LLVM IR-based CFG parsing plus configuration, validation, and tests to support selecting and running either backend.

Sequence diagram for selecting GCC vs LLVM coverage backend in runFuzz

sequenceDiagram
    participant RunFuzz
    participant Config
    participant GCCCoverage
    participant LLVMCoverage
    participant Engine

    RunFuzz->>Config: read Compiler.CoverageBackend
    alt CoverageBackend == llvm
        RunFuzz->>RunFuzz: toLLVMTargets
        RunFuzz->>LLVMCoverage: NewLLVMCoverage
        RunFuzz-->>Engine: Coverage (LLVMCoverage)
    else CoverageBackend == gcc (default)
        RunFuzz->>GCCCoverage: NewGCCCoverage
        RunFuzz-->>Engine: Coverage (GCCCoverage)
    end

    Engine->>Engine: extractCoveredLines
    Engine->>Engine: type assert FilteredLineExtractor
    Engine->>LLVMCoverage: ExtractCoveredLinesFiltered
    Engine-->>Engine: []string
Loading

File-Level Changes

Change Details Files
Introduce LLVM source-based coverage implementation and wire it as an alternative Coverage backend to GCCCoverage.
  • Add LLVMCoverageConfig/LLVMCoverage types implementing Coverage, PreCompileCoverage, PostCompileCoverage, and FilteredLineExtractor using llvm-profdata/llvm-cov and per-seed JSON reports.
  • Implement LLVMReport and llvmTotalReport on-disk format for accumulating covered file:line sets, including parsing llvm-cov export JSON and tracking covered segments.
  • Implement coverage increase detection, merging logic, stats reporting, and filtered covered-line extraction for LLVM coverage, including demangler integration and target-function-based filtering.
internal/coverage/llvm.go
internal/coverage/llvm_report.go
internal/coverage/coverage.go
Extend configuration to support selecting LLVM vs GCC coverage backends and validating required fields.
  • Add coverage_backend switch with default 'gcc' and new LLVM-specific fields (llvm_profile_dir, llvm_profdata_command, llvm_cov_command, llvm_demangler_command, fuzz.llvm_ir_paths).
  • Implement validateCoverageBackend and invoke it in LoadConfig, including tests for valid/invalid combinations and defaulting logic.
  • Document a sample LLVM/clang configuration in a new clang-v18.1.0-x64-canary.yaml file.
internal/config/config.go
internal/config/config_test.go
configs/clang-v18.1.0-x64-canary.yaml
Wire LLVM backend and analyzer selection into the fuzzing app, keeping GCC as default and encapsulating GCC analyzer construction.
  • Update runFuzz to choose coverage backend based on cfg.Compiler.CoverageBackend, constructing LLVMCoverage with per-seed LLVM_PROFILE_FILE and profile directory, or GCCCoverage otherwise.
  • Refactor GCC analyzer creation into buildGCCAnalyzer and introduce toLLVMTargets helper for converting config targets to LLVMTarget.
  • Change analyzer initialization logic to use LLVM IR parsing path when coverage_backend=llvm and GCC CFG parsing path otherwise, with shared mapping path handling and logging.
cmd/defuzz/app/fuzz.go
Introduce LLVM IR-based CFG parsing and reuse existing analyzer logic via a new constructor.
  • Implement ParseLLVMIRFiles and supporting helpers to parse LLVM .ll files into CFGFunction/BasicBlock structures with successors and line numbers using DILocation/DIFile metadata.
  • Add NewAnalyzerFromCFGFunctions to build an Analyzer from pre-parsed CFGFunction maps, reusing indexing, predecessor maps, target validation, and coverage mapping.
  • Add tests and LLVM IR fixtures (stackprotector_sample.ll, llvm_cov_sample.json) plus integration tests to verify basic block parsing, target selection, analyzer behavior, and LLVM coverage end-to-end.
internal/coverage/analyzer.go
internal/coverage/llvm_cfg.go
internal/coverage/llvm_cfg_test.go
internal/coverage/llvm_integration_test.go
internal/coverage/artifacts/stackprotector_sample.ll
internal/coverage/artifacts/llvm_cov_sample.json
Make the fuzzing engine’s covered-line extraction backend-agnostic via a new FilteredLineExtractor interface.
  • Define FilteredLineExtractor interface in coverage package and implement it for GCCCoverage and LLVMCoverage so engine can call ExtractCoveredLinesFiltered without type assertions on concrete coverage types.
  • Update Engine.extractCoveredLines to use the new FilteredLineExtractor type assertion instead of GCCCoverage-specific logic.
internal/coverage/coverage.go
internal/fuzz/engine.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 2 other issues, and left some high level feedback:

Security issues:

  • Detected non-static command inside Command. Audit the input to 'exec.Command'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code. (link)

General comments:

  • In the LLVM compile path you call os.Setenv("LLVM_PROFILE_FILE", ...) without restoring the previous value, which can leak into later compilations or other parts of the process; consider saving and restoring the old value or scoping this via the command environment instead.
  • The llvm-profdata/llvm-cov invocations build shell strings with fmt.Sprintf and "sh -c" (e.g. using %s/*.profraw and redirects), which will break on paths with spaces and is vulnerable to injection; prefer passing arguments directly via the executor (no shell) or at least quote/escape paths robustly.
  • There are two separate demangling paths (LLVMCoverage.demangleFunctionNames using exec.Executor and llvm_cfg.demangleNames using os/exec) that implement similar logic; consolidating them into a single helper would reduce duplication and keep behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the LLVM compile path you call os.Setenv("LLVM_PROFILE_FILE", ...) without restoring the previous value, which can leak into later compilations or other parts of the process; consider saving and restoring the old value or scoping this via the command environment instead.
- The llvm-profdata/llvm-cov invocations build shell strings with fmt.Sprintf and "sh -c" (e.g. using %s/*.profraw and redirects), which will break on paths with spaces and is vulnerable to injection; prefer passing arguments directly via the executor (no shell) or at least quote/escape paths robustly.
- There are two separate demangling paths (LLVMCoverage.demangleFunctionNames using exec.Executor and llvm_cfg.demangleNames using os/exec) that implement similar logic; consolidating them into a single helper would reduce duplication and keep behavior consistent.

## Individual Comments

### Comment 1
<location path="internal/coverage/llvm.go" line_range="419" />
<code_context>
+
+// demangleFunctionNames returns a map of mangled -> demangled name. If no
+// demangler is configured (or it fails), names map to themselves.
+func (l *LLVMCoverage) demangleFunctionNames(export *llvmCovExport) map[string]string {
+	names := make([]string, 0)
+	seen := make(map[string]bool)
</code_context>
<issue_to_address>
**suggestion:** Consolidate demangling logic to avoid duplicated, slightly diverging implementations

Similar demangling logic exists here (via executor.Run) and in llvm_cfg.go (demangleNames using os/exec). To avoid subtle drift in behavior (e.g., error handling, whitespace, argument limits), please extract a shared demangling helper in this package and have both LLVMCoverage and the CFG parser call it with a consistent failure and name-mapping contract.
</issue_to_address>

### Comment 2
<location path="internal/config/config.go" line_range="590-591" />
<code_context>
 		cfg.Compiler.Oracle.Options = make(map[string]interface{})
 	}

+	// Coverage backend: default to "gcc" for backward compatibility.
+	if cfg.Compiler.CoverageBackend == "" {
+		cfg.Compiler.CoverageBackend = "gcc"
+	}
</code_context>
<issue_to_address>
**suggestion:** Normalize coverage_backend casing before validation to avoid surprising config errors

`validateCoverageBackend` currently expects a normalized value, but `LoadConfig` only defaults empty strings and otherwise passes the value through. Mixed-case values like `"LLVM"` or `"Gcc"` will therefore hit the default case and be rejected as unknown. Normalizing (e.g., `strings.ToLower`, and optionally `strings.TrimSpace`) before calling `validateCoverageBackend` would avoid these surprising config failures while preserving behavior.

Suggested implementation:

```golang
	// Coverage backend: normalize casing and default to "gcc" for backward compatibility.
	cfg.Compiler.CoverageBackend = strings.ToLower(strings.TrimSpace(cfg.Compiler.CoverageBackend))
	if cfg.Compiler.CoverageBackend == "" {
		cfg.Compiler.CoverageBackend = "gcc"
	}

```

1. Ensure `internal/config/config.go` imports the `strings` package, e.g.:
   - Add `strings` to the existing import block: `import ( ... "strings" )`.
2. Confirm that any call to `validateCoverageBackend(cfg)` in `LoadConfig` happens after this normalization block so that validation always sees a normalized `CoverageBackend` value.
</issue_to_address>

### Comment 3
<location path="internal/coverage/llvm_cfg.go" line_range="340" />
<code_context>
	cmd := osexec.Command(command, names...)
</code_context>
<issue_to_address>
**security (go.lang.security.audit.dangerous-exec-command):** Detected non-static command inside Command. Audit the input to 'exec.Command'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/coverage/llvm.go

// demangleFunctionNames returns a map of mangled -> demangled name. If no
// demangler is configured (or it fails), names map to themselves.
func (l *LLVMCoverage) demangleFunctionNames(export *llvmCovExport) map[string]string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consolidate demangling logic to avoid duplicated, slightly diverging implementations

Similar demangling logic exists here (via executor.Run) and in llvm_cfg.go (demangleNames using os/exec). To avoid subtle drift in behavior (e.g., error handling, whitespace, argument limits), please extract a shared demangling helper in this package and have both LLVMCoverage and the CFG parser call it with a consistent failure and name-mapping contract.

Comment thread internal/config/config.go
Comment on lines +590 to +591
// Coverage backend: default to "gcc" for backward compatibility.
if cfg.Compiler.CoverageBackend == "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Normalize coverage_backend casing before validation to avoid surprising config errors

validateCoverageBackend currently expects a normalized value, but LoadConfig only defaults empty strings and otherwise passes the value through. Mixed-case values like "LLVM" or "Gcc" will therefore hit the default case and be rejected as unknown. Normalizing (e.g., strings.ToLower, and optionally strings.TrimSpace) before calling validateCoverageBackend would avoid these surprising config failures while preserving behavior.

Suggested implementation:

	// Coverage backend: normalize casing and default to "gcc" for backward compatibility.
	cfg.Compiler.CoverageBackend = strings.ToLower(strings.TrimSpace(cfg.Compiler.CoverageBackend))
	if cfg.Compiler.CoverageBackend == "" {
		cfg.Compiler.CoverageBackend = "gcc"
	}
  1. Ensure internal/config/config.go imports the strings package, e.g.:
    • Add strings to the existing import block: import ( ... "strings" ).
  2. Confirm that any call to validateCoverageBackend(cfg) in LoadConfig happens after this normalization block so that validation always sees a normalized CoverageBackend value.

// returning its stdout. llvm-cxxfilt and c++filt both accept names as args and
// print one demangled name per line.
func runDemangler(command string, names []string) (string, error) {
cmd := osexec.Command(command, names...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (go.lang.security.audit.dangerous-exec-command): Detected non-static command inside Command. Audit the input to 'exec.Command'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

Source: opengrep

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant