Repository navigation
PGO instrumentation: counter in num_bigint::div_rem_core incremented by a heap address instead of 1 #164635
Description
Summary
With rustc 1.99.1 (LLVM 23.1.3), a -Cprofile-generate build of a small program using num-bigint 0.4.8 writes a raw profile in which one counter of num_bigint::biguint::division::div_rem_core holds a value around 1.5e17. llvm-profdata merge then fails:
warning: train.profraw: excessively large counter value suggests corrupted profile data: 154409276267866944
error: no profile can be merged
The profile file itself is well formed (format version 11). Its 191 records cover the 4277 counters with no gap, and exactly one counter is wrong (115 of the 117 counters of div_rem_core). The largest other counter is 123816. The instrumented code adds the value of a register that holds a heap pointer to that counter, instead of adding 1 (or a 0/1 flag). It does this each time the block runs, and 1636 executions times an address near 0x5585_xxxx_xxxx gives the observed ~1.54e17.
The same source builds a correct profile with rustc 1.97.0 (LLVM 22.1.6).
Environment
rustc 1.99.1 (58cf8f9c3 2026-10-13), Arch Linux packagerust 1:1.99.1-1, LLVM 23.1.3llvm-profdata23.1.3 (same LLVM version)- host: x86_64-unknown-linux-gnu
- Not reproduced with rustc 1.97.0 / LLVM 22.1.6 (rustup), same sources and flags
Reproduction
Cargo.toml:
[package]
name = "pidigits"
version = "0.1.0"
edition = "2021"
[dependencies]
num-bigint = "0.4" # 0.4.8 in Cargo.lock
num-traits = "0.2" # 0.2.19
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"src/main.rs (111 lines)
// pidigits, CLBG Rust #4 (TeXitoi, Ryohei Machida, 3-Clause BSD), with num-bigint instead of rug
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::cmp;
use std::io::Write;
pub struct Context {
q: BigInt,
r: BigInt,
t: BigInt,
tmp1: BigInt,
k: u32,
}
impl Context {
pub fn new() -> Context {
Context {
q: BigInt::from(1),
r: BigInt::from(0),
t: BigInt::from(1),
tmp1: BigInt::from(0),
k: 0,
}
}
pub fn next(&mut self) -> u8 {
loop {
self.next_term();
if self.q > self.r {
continue;
}
let d = self.extract(3);
if d != self.extract(4) {
continue;
}
self.produce(d);
return d;
}
}
fn next_term(&mut self) {
self.k += 1;
let k2 = self.k * 2 + 1;
self.tmp1.clone_from(&self.q);
self.tmp1 <<= 1u32;
self.r += &self.tmp1;
self.r *= k2;
self.t *= k2;
self.q *= self.k;
}
#[inline]
fn extract(&mut self, nth: u32) -> u8 {
if nth.is_power_of_two() {
// use shift operation if possible
self.tmp1.clone_from(&self.q);
self.tmp1 <<= nth.trailing_zeros();
} else {
self.tmp1.clone_from(&self.q);
self.tmp1 *= nth;
};
self.tmp1 += &self.r;
(&self.tmp1 / &self.t).to_u8().unwrap()
}
fn produce(&mut self, n: u8) {
self.q *= 10u8;
self.tmp1.clone_from(&self.t);
self.tmp1 *= n;
self.r -= &self.tmp1;
self.r *= 10u8;
}
}
fn main() {
let n: usize = std::env::args_os()
.nth(1)
.and_then(|s| s.into_string().ok())
.and_then(|n| n.parse().ok())
.unwrap_or(27);
let mut ctx = Context::new();
// line buffer
let mut line_buf = [0u8; 12];
line_buf[10] = b'\t';
line_buf[11] = b':';
// output buffer
let mut output = Vec::with_capacity(n * 2);
for d in (0..n).step_by(10) {
let count = cmp::min(10, n - d);
for i in 0..count {
line_buf[i] = b'0' + ctx.next();
}
for i in count..10 {
line_buf[i] = b' ';
}
output.extend_from_slice(&line_buf);
let _ = writeln!(output, "{}", d + count);
}
let _ = std::io::stdout().write_all(&*output);
}RUSTFLAGS="-Cprofile-generate=$PWD/prof" cargo build --release
LLVM_PROFILE_FILE=$PWD/prof/train.profraw target/release/pidigits 200 > /dev/null
llvm-profdata merge -o merged.profdata prof/train.profrawExpected: the merge succeeds.
Actual: excessively large counter value ... error: no profile can be merged. The bad value changes from run to run (1.537e17 to 1.549e17), because it is a sum of heap addresses.
It also fails with each of the following changes, made one at a time:
- without
-C target-cpu=native; lto = falseorlto = "thin";codegen-units = 16;panic = "unwind";opt-level = 2;- all of the above at once.
Analysis
In the instrumented binary, counters 114 to 116 of div_rem_core are updated without branches. Counters 114 and 116 get the 0/1 flag rdx == -1 (sete %r8b). Counter 115 is updated in the same 128-bit vpaddq as 116, and it should also get a 0/1 value. Instead, it gets %rax, which is the pointer that the same condition selects for the following store (cmovne %rax,%rsi / mov %rcx,(%rsi,%r10,8)):
xor %edi,%edi
mov %rax,%rsi
cmp $0xffffffffffffffff,%rdx
sete %r8b
je 2d89b
mov 0x80(%rsp),%rsi
mov %r8b,%dil
add %rdi,0x67ea3(%rip) # 95748 ; counter #114
cmp %rsi,%r10
jae 2e189
test $0x1,%al
mov $0x8,%esi
lea 0x80(%rsp),%r8
cmovne %r8,%rsi
vmovq %rax,%xmm0
vmovd %edi,%xmm1
vpunpcklqdq %xmm1,%xmm0,%xmm0
vpaddq 0x67e7a(%rip),%xmm0,%xmm0 # 95750 ; counters #115, #116
vmovdqu %xmm0,0x67e72(%rip) # 95750 ; counters #115, #116
cmp $0xffffffffffffffff,%rdx
cmovne %rax,%rsi
mov %rcx,(%rsi,%r10,8)Under gdb, %rax at vmovq %rax,%xmm0 is 0x5555555fc7b0, then 0x5555555fc7e0 (heap addresses). The same instrumented binary produces the bad profile on two different machines, so the cause is the generated code, not the environment.
What avoids it (instrumented build only, LLVM 23.1.3)
| Extra flags | Result |
|---|---|
-Cllvm-args=-vectorize-slp=false |
still corrupt |
-Cllvm-args=-do-counter-promotion=false |
still corrupt |
-Cllvm-args=-instrprof-atomic-counter-update-all |
still corrupt |
-Cllvm-args=-two-entry-phi-node-folding-threshold=0 -Cllvm-args=-phi-node-folding-threshold=0 |
OK |
-Cllvm-args=-speculate-one-expensive-inst=false -Cllvm-args=-two-entry-phi-node-folding-threshold=0 |
OK |
opt-level = 1 for num-bigint only |
OK |
opt-level = 1 or 0 for everything |
OK |
This points at SimplifyCFG's two-entry PHI folding (foldTwoEntryPHINode) rather than SLP or counter promotion.
Only the profile counters were seen to be wrong, and the program output is correct. But this is a miscompile, so non-instrumented code using the same pattern may be affected as well.
I can provide the raw profile and the instrumented binary on request.
Activity
nikic commentedon Oct 20, 2026
This sounds like a duplicate of #163616, which is fixed on beta. Can you still reproduce the issue on beta?
maxdexh commentedon Oct 20, 2026
Oops, my bad, I thought I initialized rustup in my VM using
default nightlywhile verifying, but I did stable instead.This doesn't reproduce on nightly