Skip to content

Latest commit

 

History

History
123 lines (101 loc) · 8.52 KB

File metadata and controls

123 lines (101 loc) · 8.52 KB

Looking for new bugs

The edits and the replayed regressions test whether mirth catches bugs that someone put in. This tests whether it finds bugs nobody knew were there: the unmodified pinned compiler (ea137335b), a wider fixture, and more incremental edits.

fixtures/wide has a proc-macro crate (a derive and an attribute macro), a library with a build script that generates code, built-in derives, const generics, a generic associated type, impl Trait and async fn in traits, and an exported macro_rules!, used by a second library and a binary. fixtures/wide/edits has ten changes: a private body, a new enum variant, a doc comment, spans only, two items swapped, the derive's output, the build script's output, a const generic argument, a macro's expansion, and a new private function.

rustc/hunt.sh <fixture> builds the fixture eight times with -Zthreads=8 and compares the .rmeta files (P5). Then, for each edit, it compares an incremental rebuild after the edit with a clean build of the edited source (P6), with -Zincremental-verify-ich, single-threaded and with -Zthreads=8. rustc/check.sh wide runs the ordinary checks.

Findings

what status
1 incremental rebuilds encode Generics::param_def_id_to_index in a different order from clean builds looks new; root cause found; fix and regression test written
2 incremental rebuilds encode a string literal twice where clean builds encode it once looks new; root cause found, regression from #116707 (1.90); fix and regression test written
3 with -Zthreads=8, two traits with -> impl Trait methods give different metadata from run to run known: #162202; a testing workaround, hunt/threads-def-order-stopgap.patch, makes the order deterministic
4 incremental rebuilds republish the previous session's metadata when an edit moves no span, so its source map describes old files looks new; found later by the fuzzer and the history replay (scale.md); root cause found, regression from #114669 (1.90); fix and regression test written
5 six untracked options change results incremental compilation reuses; with -Zno-leak-check, a rebuild accepts a program a clean build rejects looks new; the first three found by a query written from a closed bug and an option audit (ur-queries.md), -Zno-leak-check, -C extra-filename and -Zfuture-incompat-test by reporting untracked reads (untracked-reads.md); report drafted
6 reused object code keeps the previous checksum of an edited source file in its debuginfo, and with -Zembed-source the previous file; with optimizations, ThinLTO symbol names then differ from a clean build looks new; found by the fuzzer at -Copt-level=2; root cause found, since 1.44 (#69718); report drafted and a regression test (hunt/tests/incr-debuginfo-embedded-source, failing: no fix)
7 warnings from inline assembly are not shown again when an incremental rebuild reuses the codegen unit looks new; found while checking reused codegen units (shadow-mode.md); on 1.60.0 through the nightly; report drafted (draft) and a regression test (hunt/tests/incr-asm-warning-reused, failing: no fix)
8 with -Zmir-opt-level=3 and debuginfo, an incremental rebuild encodes an allocation twice where a clean build encodes it once looks new; found by the fuzzer at -Zmir-opt-level=4 and named by the reuse check, reduced to three lines; on 1.60.0 through the nightly; report drafted (draft), no fix

Findings 1 and 2 are single-threaded: an ordinary cargo build, an edit, another cargo build, and the metadata differs from a clean build of the edited source. Both come from the same kind of mistake: a value that does not survive a round trip through the incremental cache unchanged. All three reproduce with the official nightly-2026-10-06, without mirth: docs/hunt/repro.sh runs them. None was searched for: P5 and P6 reported them on the first run of the new fixture.

Draft bug reports for 1, 2, 4, 5, 6, 7 and 8, written to be filed upstream, are hunt/issue-generics-order.md, hunt/issue-literal-dedup.md and hunt/issue-stale-metadata-reuse.md and hunt/issue-untracked-options.md, the last a comment for rust-lang/rust#84232; for 6 it is hunt/issue-stale-debuginfo-source.md, which has no fix, since every fix costs codegen reuse and the choice is the maintainers'. Each has a candidate fix (hunt/*.patch) and a regression test in the style of rustc's tests/run-make (hunt/tests/), which fails on the pinned compiler and passes with the fix.

With both fixes applied, P6 holds for all ten edits single-threaded, and these rustc tests still pass: tests/incremental, the metadata-related UI and run-make tests, tests/ui/{consts,statics,const-generics} and tests/codegen-llvm.

Everything else held: P1, P2, P4 and P7 on the recorded clean build; the touch-only rebuild reused every crate's metadata; and -Zincremental-verify-ich found no unstable fingerprint in 20 incremental rebuilds.

1. param_def_id_to_index order

pub struct Grid<A, B, C>(A, B, C);
impl<A, B, C> Grid<A, B, C> { pub const AREA: usize = 1; }

Generics::param_def_id_to_index is an FxHashMap. HashMap's Encodable writes it in iteration order, and Decodable collects it back in that order. In this example all three keys hash to the same home bucket of a 4-bucket table, so iteration order is insertion order rotated by one. Each time generics_of goes through the incremental cache, the map is rotated again, and the metadata encoder writes it in the rotated order. The encoded order cycles with period 3 over successive incremental rebuilds, which is exactly what a standalone program with std HashMap and rustc_hash 2.1.1 predicts.

Adding a comment to the top of lib.rs changes the metadata of an incremental rebuild relative to a clean one for either, smallvec, memchr and arrayvec, and making the field an FxIndexMap removes the difference in all four. The same field is the cause given in #163878 under -Zthreads.

2. String literals encoded twice

#[inline] pub fn a() -> &'static str { "literal" }
#[inline] pub fn b() -> &'static str { "literal" }   // then edited to `{ let s = "literal"; s }`

String literals get their AllocId from allocate_bytes_dedup, so both bodies share one, and a clean build encodes the allocation once. In the rebuild, a's MIR comes from the incremental cache, and decoding a memory allocation always reserves a fresh AllocId (reserve_and_set_memory_alloc). The metadata then encodes two allocations with identical bytes. It started with #116707, which gave ConstValue::Slice an AllocId: nightly-2025-07-24 is not affected, nightly-2025-07-26 is. No effect on generated code was found.

In the fixture, the two literals were the type name in #[derive(Debug)]'s fmt (which is #[inline], so its MIR is encoded) and the same name in a const from the fixture's own derive. That is why removing either derive made it disappear. The first write-up of this finding blamed hygiene data, because the rebuild resolved foreign expansions while encoding; that was a side effect of decoding cached MIR, not the cause.

3. impl Trait in traits under -Zthreads

pub trait Render { fn render(&self) -> impl Sized; }
pub trait Draw { fn draw(&self) -> impl Sized; }

Eight builds with -Zthreads=8 give three or four different .rmeta files. A comment on #162202 reports return-position impl Trait in traits as not reproducible, so this is known. The fix for finding 1 does not change it. Both fixtures have such traits, so P5 under -Zthreads=8 fails on wide every time, and on chain occasionally (1 build in 24).

Not done

  • Neither new finding has been reported upstream. The drafts are ready; check #163878 again before filing the first, since it touches the same field.
  • fixtures/wide fails check.sh (P5 under -Zthreads=8, finding 3) and P6 for most edits (findings 1 and 2) until those are fixed. Its lists are blessed against the unmodified compiler.