Skip to content

MergeFunctions + -Cpasses=sancov-module leaves coverage counters without a PC table (breaks cargo fuzz --build-std) #163737

Closed

Description

@lbrooney

With -Cpasses=sancov-module plus the inline-8bit-counters and pc-table options (what cargo-fuzz uses), identical weak functions end up with coverage counters but no PC table after linking with rust-lld. libFuzzer refuses to start such binaries:

ERROR: The size of coverage PC tables does not match the
number of instrumented PCs. This might be a compiler bug,
please contact the libFuzzer developers.

Reproducer

The two hook functions stand in for libFuzzer and perform its startup check.

#![feature(linkage)]

use std::sync::atomic::{AtomicUsize, Ordering};

// Two identical weak functions: MergeFunctions folds them into one private
// function and turns both names into aliases of it.
#[linkage = "weak"]
#[unsafe(no_mangle)]
pub extern "C" fn first(x: i32) -> i32 {
    if x > 3 { x * 7 } else { x - 1 }
}

#[linkage = "weak"]
#[unsafe(no_mangle)]
pub extern "C" fn second(x: i32) -> i32 {
    if x > 3 { x * 7 } else { x - 1 }
}

// Stand-ins for the libFuzzer runtime hooks, which receive the linked
// __sancov_cntrs and __sancov_pcs sections.
static COUNTERS: AtomicUsize = AtomicUsize::new(0);
static PCS: AtomicUsize = AtomicUsize::new(0);

#[unsafe(no_mangle)]
pub unsafe extern "C" fn __sanitizer_cov_8bit_counters_init(start: *const u8, end: *const u8) {
    COUNTERS.store(end as usize - start as usize, Ordering::Relaxed);
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn __sanitizer_cov_pcs_init(start: *const usize, end: *const usize) {
    // Each PC table entry is a (pc, flags) pair.
    PCS.store((end as usize - start as usize) / (2 * size_of::<usize>()), Ordering::Relaxed);
}

fn main() {
    println!("first(argc) = {}", first(std::env::args().count() as i32));
    let (counters, pcs) = (COUNTERS.load(Ordering::Relaxed), PCS.load(Ordering::Relaxed));
    println!("counters = {counters}, pc table entries = {pcs}");
    if counters != pcs {
        println!("MISMATCH: libFuzzer would abort here");
        std::process::exit(1);
    }
}
rustc +nightly -O -Ccodegen-units=1 -Cpasses=sancov-module \
  -Cllvm-args=-sanitizer-coverage-level=3 \
  -Cllvm-args=-sanitizer-coverage-inline-8bit-counters \
  -Cllvm-args=-sanitizer-coverage-pc-table \
  repro.rs && ./repro

I expected the counter and PC table counts to match. Instead:

first(argc) = 0
counters = 26, pc table entries = 25
MISMATCH: libFuzzer would abort here

With -Zmerge-functions=disabled added, both counts are 26. -Ccodegen-units=1, which cargo-fuzz also passes, is needed so both functions are in the same LLVM module.

A rustc built with LLVM assertions (the alt CI build of the same commit) aborts while compiling the reproducer instead, and also while compiling compiler_builtins with -Zbuild-std and these flags:

Instrumentation.cpp:99: Comdat *llvm::getOrCreateFunctionComdat(Function &, Triple &): Assertion `F.hasName()' failed.

Cause

MergeFunctions, enabled by default at opt-level 2 and above, folds the two weak functions into an unnamed private function. rustc runs -Cpasses after its optimization pipeline, so SanitizerCoverage then instruments that unnamed function and puts its arrays in an empty-named comdat, which is never emitted as a section group. rust-lld's --gc-sections keeps the counters, which the code references, and discards the PC table. Details and an LLVM-only reproducer: llvm/llvm-project#228820. Clang isn't affected because it instruments before MergeFunctions runs.

Real-world impact

cargo fuzz --build-std (also implied by --careful, and used for the memory and thread sanitizers) instruments compiler_builtins. Its identical weak intrinsics, for example rint/roundeven and __eqdf2/__ledf2, are merged this way, so any fuzz target that links one of them fails at startup. In my project, a ThreadSanitizer target failed with 68,741 counters against 68,642 PC entries. The 99 missing entries are exactly the merged functions' arrays from compiler_builtins.

Workaround: add -Zmerge-functions=disabled to RUSTFLAGS for these builds. rustc could also avoid this by disabling MergeFunctions when -Cpasses contains sancov-module.

rust-fuzz/cargo-fuzz#197 tracks the same libFuzzer error, but it predates this and had a different cause (old GNU ld).

Meta

rustc --version --verbose:

rustc 1.101.0-nightly (c36f14571 2026-10-01)
binary: rustc
commit-hash: c36f1457196e315bc204b9564a6a5a7fe7f5a51f
commit-date: 2026-10-01
host: x86_64-unknown-linux-gnu
release: 1.101.0-nightly
LLVM version: 23.1.1

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 4, 2026
  2. added
    llm-assistedAn LLM-assisted PR as defined by the LLM policy. Requires ahead-of-time consent by assignee.
    and removed
    llm-assistedAn LLM-assisted PR as defined by the LLM policy. Requires ahead-of-time consent by assignee.
    on Oct 4, 2026
  3. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    I-miscompileIssue: Correct Rust code lowers to incorrect machine code
    F-linkage#![feature(linkage)]`
    C-bugCategory: This is a bug.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 4, 2026
  4. maxdexh commented on Oct 4, 2026

    @maxdexh
    Member

    IIUC, this is an LLVM bug?

  5. nikic commented on Oct 4, 2026

    @nikic
    Contributor

    LLVM should handle this more gracefully, but I believe the issue wouldn't occur if the sancov pass were scheduled at the pipeline position where it is supposed to run (i.e. the same as all the other sanitizers). See also #161855 (comment).

  6. lbrooney commented on Oct 6, 2026

    @lbrooney
    Author

    The underlying issue should be fixed in llvm/llvm-project#228959

    Had the pipelining been different then this issue might not have been encountered, but that's a broader discussion I'm not equipped to have.

  7. added
    llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes
    on Oct 13, 2026
  8. added 2 commits that reference this issue on Oct 14, 2026
  9. linked a pull request that will close this issue[beta] backports #164371on Oct 16, 2026
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

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.F-linkage#![feature(linkage)]`I-miscompileIssue: Correct Rust code lowers to incorrect machine codellvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions