Repository navigation
release-mode heap corruption with non-generic FnOnce(Vec<usize>) #155241
Description
I tried this code:
#![cfg_attr(not(test), no_std)]
extern crate alloc;
use alloc::vec::Vec;
fn with(f: impl FnOnce(Vec<usize>)) {
f(Vec::new())
}
#[test]
fn repro_len_ne_unreachable() {
with(|mut v| v.resize(2, 1));
with(|v| {
if v.len() != 0 {
unreachable!();
}
});
}I expected to see this happen: the test should pass, because each with call receives a fresh empty Vec<usize>, so the second closure should observe v.len() == 0 and never enter the unreachable!() branch.
Instead, this happened: in --profile release, the test process aborts due to allocator corruption.
Observed results:
Windows x86_64-pc-windows-msvc:
process aborts with 0xc0000374 (STATUS_HEAP_CORRUPTION)
Linux x86_64-unknown-linux-gnu (Docker rust:latest image):
free(): double free detected in tcache 2
process aborts with SIGABRT
Two nearby controls do not reproduce:
fn with_value<T>(f: impl FnOnce(Vec<T>)) {
f(Vec::new())
}
#[test]
fn control_generic_helper() {
with_value(|mut v: Vec<usize>| v.resize(2, 1));
with_value(|v: Vec<usize>| {
if v.len() != 0 {
unreachable!();
}
});
}
#[test]
fn repro_len_ne_unreachable_unchecked() {
with(|mut v| v.resize(2, 1));
with(|v| {
if v.len() != 0 {
unsafe {
core::hint::unreachable_unchecked();
}
}
});
}Commands used:
cargo +nightly miri test repro_len_ne_unreachable -- --exact --nocapture
$env:CARGO_TARGET_DIR='target/report-repro'; cargo test --profile release repro_len_ne_unreachable -- --exact --nocapture
$env:CARGO_TARGET_DIR='target/report-control-generic'; cargo test --profile release control_generic_helper -- --exact --nocapture
$env:CARGO_TARGET_DIR='target/report-control-unchecked'; cargo test --profile release repro_len_ne_unreachable_unchecked -- --exact --nocapture
docker run --rm -v ${PWD}:/work -w /work rust:latest cargo test --profile release repro_len_ne_unreachable -- --exact --nocapture
Miri did not report UB on an earlier form of this reproducer.
Version history:
stable-1.74.1: ok
stable-1.75.0: ok
stable-1.76.0: ok
stable-1.77.0: first wrong result, panics at `unreachable!()`
stable-1.85.0: still panics at `unreachable!()`
stable-1.90.0: still panics at `unreachable!()`
stable-1.91.0: first stable with STATUS_HEAP_CORRUPTION
stable-1.92.0: STATUS_HEAP_CORRUPTION
stable-1.93.0: STATUS_HEAP_CORRUPTION
stable-1.94.0: STATUS_HEAP_CORRUPTION
nightly-2026-04-12: STATUS_HEAP_CORRUPTION
Nightly boundaries I checked:
last good nightly: nightly-2024-01-16, rustc 1.77.0-nightly (714b29a17 2024-01-15)
first bad nightly: nightly-2024-01-17, rustc 1.77.0-nightly (098d4fd74 2024-01-16), wrong panic at `unreachable!()`
earliest tested nightly with heap corruption: nightly-2024-01-18, rustc 1.77.0-nightly (6ae4cfbbb 2024-01-17)
last tested nightly with wrong panic before the later stable corruption window: nightly-2025-08-12, rustc 1.91.0-nightly (1ebbd87a6 2025-08-11)
first tested nightly with heap corruption in that window: nightly-2025-08-14, rustc 1.91.0-nightly (3672a55b7 2025-08-13)
So on stable, the bug first appears as an incorrect panic in 1.77.0 and becomes heap corruption in 1.91.0. On nightly, the first wrong result is between 2024-01-16 and 2024-01-17.
Meta
rustc --version --verbose:
rustc 1.94.0 (4a4ef493e 2026-03-02)
binary: rustc
commit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db
commit-date: 2026-03-02
host: x86_64-pc-windows-msvc
release: 1.94.0
LLVM version: 21.1.8
Nightly also reproduces:
rustc 1.97.0-nightly (14196dbfa 2026-04-12)
binary: rustc
commit-hash: 14196dbfa3eb7c30195251eac092b1b86c8a2d84
commit-date: 2026-04-12
host: x86_64-pc-windows-msvc
release: 1.97.0-nightly
LLVM version: 22.1.2
Linux container reproduction:
running 1 test
free(): double free detected in tcache 2
error: test failed, to rerun pass `--lib`
Caused by:
process didn't exit successfully: `/work/target/release/deps/repro_test-9864353ff121d9c3 repro_len_ne_unreachable --exact --nocapture` (signal: 6, SIGABRT: process abort signal)
Additional stable versions checked:
rustc 1.74.1 (a28077b28 2023-12-04)
rustc 1.75.0 (82e1608df 2023-12-21)
rustc 1.76.0 (07dca489a 2024-02-04)
rustc 1.77.0 (aedd173a2 2024-03-17)
rustc 1.85.0 (4d91de4e4 2025-02-17)
rustc 1.90.0 (1159e78c4 2025-09-14)
rustc 1.91.0 (f8297e351 2025-10-28)
rustc 1.92.0 (ded5c06cf 2025-12-08)
rustc 1.93.0 (254b59607 2026-01-19)
Backtrace
I reran the reproducer with RUST_BACKTRACE=1, but there was still no Rust backtrace.
The process aborts immediately with:
running 1 test
error: test failed, to rerun pass `--lib`
Caused by:
process didn't exit successfully: `C:\Users\akrut\repro_test\target/report-backtrace\release\deps\repro_test-1f8b8a2035f2538a.exe repro_len_ne_unreachable --exact --nocapture` (exit code: 0xc0000374, STATUS_HEAP_CORRUPTION)
Here's a miscompile in debug mode (should print 0, but prints 1):
#![feature(fn_traits)]
#![expect(unused)]
#[derive(Copy, Clone)]
struct Thing {
x: usize,
y: usize,
z: usize,
}
fn main() {
let value = (Thing { x: 0, y: 0, z: 0 },);
(|mut thing: Thing| {
thing.z = 1;
}).call(value);
println!("{}", value.0.z)
}The miscompilation occurs with .call() and .call_mut() but not .call_once().
Using RUSTC_BOOTSTRAP=1, I found that this miscompilation was a regression from 1.68.0 (prints 0) to 1.69.0 (prints 1).
It appears that rust is passing an arguent to the closure by passing a pointer that points to value. The closure then mutates through the pointer. The…
Activity
bjorn3 commentedon Apr 13, 2026
This needs at least
Zmir-opt-level=2and-Copt-level=2to reproduce. Reducing either opt level to 1 makes the program work fine. And it doesn't reproduce in miri even with MIR optimizations enabled.matthiaskrgr commentedon Apr 13, 2026
relevant mir passes:
-Zmir-enable-passes=+DataflowConstProp -Zmir-enable-passes=+GVN -Zmir-enable-passes=+Inlinematthiaskrgr commentedon Apr 13, 2026
If I read the git log correctly, #119439 falls into this window, @cjgillot can you see if this is related?
saethlin commentedon Apr 14, 2026
Anyone interested can do a few things: look at exactly what transformation is being produced by GVN using -Zdump_mir=all, and use LLVM's opt-bisect-limit to identify the LLVM transformation at which things go wrong.
@matthiaskrgr you said DataFlowConstProp is required but the rest of this issue is about -Zmir-opt-level=2. That's very strange
matthiaskrgr commentedon Apr 14, 2026
🤔
I ran
with
matthiaskrgr commentedon Apr 14, 2026
with
-Zmir-opt-level=2 -Copt-level=2I can reproduce it with codegen backends llvm, gcc but not craneliftpitust commentedon Apr 14, 2026
Reproducer without
vectheemathas commentedon Apr 14, 2026
Another reproducer (prints 0 in debug mode, prints 1 in release mode)
23 remaining items
RalfJung commentedon Apr 15, 2026
Okay, thanks. #155296 should likely be closed then as it has nothing to do with the actual problem.
So a copy somehow turns into a by-move argument during codegen? Uh-oh.
RalfJung commentedon Apr 15, 2026
Would be good to have a custom MIR testcase with this pattern, to sidestep any dependency on GVN.
theemathas commentedon Apr 15, 2026
RalfJung commentedon Apr 15, 2026
pitust commentedon Apr 15, 2026
My understanding is that copying by-move arguments that use a mir
copy(and notmove) occurs here which only runs if the argument is not arust-call"untupled" argument (since the loop is overfirst_args...). Which yes, is really bad.theemathas commentedon Apr 15, 2026
Is the fix in #155291 the correct fix or not?
(The test in that PR makes zero sense though.)
dianqk commentedon Apr 15, 2026
AFAIK, I can only say the fix just works. #155343 is an attempt to clarify what exactly we should fix.
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic
Auto merge of #155343 - dianqk:indirect-by-ref, r=nikic