rust-lang / rust-lang/rust

[ICE]: unstable fingerprints for `shallow_lint_levels_on` when the registered lint set changes between incremental runs

Open
#160,019 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug I-ICE needs-triage T-compiler
Dominant language
Rust
Stars
119k
Forks
16.2k
PR merge metrics
PR metrics pending

Description

Two compilations sharing an incremental cache but registering different lints
build different ShallowLintLevelMaps from identical source. The lint needs no
pass and nothing is emitted by it — registering the name is enough.

Code

Reproducing needs a driver that can vary the lint registry. Easiest is dylint
(cargo install cargo-dylint dylint-link); any rustc_driver embedder calling
LintStore::register_lints works.

// src/lib.rs — the whole crate under test
#[allow(mwe_dylint_lint)]
pub fn f() {}

Cargo.toml:

[package]
name = "ice_mwe"
version = "0.0.0"
edition = "2024"
publish = false

[workspace]
members = []

[workspace.metadata.dylint]
libraries = [{ path = "lint_known" }, { path = "lint_other" }]

lint_known/src/lib.rs. lint_other is the same file with the names changed to
mwe_other_lint / MWE_OTHER_LINT. Neither registers a pass, so neither can
emit anything:

#![feature(rustc_private)]

extern crate rustc_lint;
extern crate rustc_session;

dylint_linting::dylint_library!();

#[unsafe(no_mangle)]
pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
    dylint_linting::init_config(sess);
    lint_store.register_lints(&[MWE_DYLINT_LINT]);
}

rustc_session::declare_lint! {
    /// Exists only to be named in an `allow`.
    pub MWE_DYLINT_LINT,
    Warn,
    "reproducer lint"
}

Each lint crate is its own workspace with crate-type = ["cdylib"],
dylint_linting = "6.0.1", rust-toolchain.toml on nightly, and a
.cargo/config.toml setting linker = "dylint-link".

Then alternate which library is loaded:

$ cargo dylint --lib lint_known
$ cargo dylint --lib lint_other
$ cargo dylint --lib lint_other
$ cargo dylint --lib lint_known

Reproduced with cargo-dylint 6.0.1. Later versions may key the target
directory on the library set, which would stop this reproducing — pin
cargo install cargo-dylint@6.0.1 dylint-link@6.0.1 if so. An issue has
also been filed against the dylint repo.

Meta

rustc --version --verbose:

rustc 1.99.0-nightly (dc3f85158 2026-07-26)
binary: rustc
commit-hash: dc3f85158a955a87a6e4363af1fbe9cf2d063cce
commit-date: 2026-07-26
host: x86_64-unknown-linux-gnu
release: 1.99.0-nightly
LLVM version: 22.1.8

Also reproduces on nightly-2026-04-16 (1.97.0-nightly, e8e4541ff).

Error output
warning: unknown lint: `mwe_dylint_lint`
 --> src/lib.rs:1:9
  |
1 | #[allow(mwe_dylint_lint)]
  |         ^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unknown_lints)]` on by default

error: internal compiler error: encountered incremental compilation error with shallow_lint_levels_on(ice_m
  |
  = note: please follow the instructions below to create a bug report with the provided information
  = note: for incremental compilation bugs, having a reproduction is vital
  = note: an ideal reproduction consists of the code before and some patch that then triggers the bug when applied and compiled again
  = note: as a workaround, you can run `cargo clean -p ice_mwe` or `cargo clean` to allow your project to c

thread 'rustc' (96246) panicked at compiler/rustc_middle/src/verify_ich.rs:82:9:
Found unstable fingerprints for shallow_lint_levels_on(ice_mwe[80d6]::f): ShallowLintLevelMap { specs: {}, expectations: [] }

Going the other way (lint_other cached, then lint_known) prints the
populated map instead, which is the other half of the mismatch:

Found unstable fingerprints for shallow_lint_levels_on(ice_mwe[80d6]::f): ShallowLintLevelMap {
  specs: {0: {LintId { lint: Lint { name: "MWE_DYLINT_LINT", default_level: Warn,
    desc: "reproducer lint", ... } }:
    LevelSpec { level: Allow, lint_id: None,
      src: Node { name: "mwe_dylint_lint", span: src/lib.rs:1:9: 1:24 (#0), reason: None } }}},
  expectations: [] }
Backtrace

thread 'rustc' (96246) panicked at compiler/rustc_middle/src/verify_ich.rs:82:9:
Found unstable fingerprints for shallow_lint_levels_on(ice_mwe[80d6]::f): ShallowLintLevelMap { specs: {},
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: rustc_middle::verify_ich::incremental_verify_ich_failed
   3: rustc_query_impl::execution::try_execute_query::<rustc_data_structures::vec_cache::VecCache<rustc_hiruery::erase::ErasedData<[u8; 8]>, rustc_middle::dep_graph::graph::DepNodeIndex>, true>
   4: rustc_lint::levels::skippable_lints
      [... omitted 1 frame ...]
   5: rustc_lint::late::check_crate::{closure#0}
   6: rustc_lint::late::check_crate
   7: rustc_interface::passes::analysis::{closure#0}::{closure#0}::{closure#2}
   8: rustc_data_structures::sync::parallel::par_fns
   9: rustc_interface::passes::analysis::{closure#0}::{closure#0}
  10: rustc_data_structures::sync::parallel::par_fns
  11: rustc_interface::passes::analysis
  12: rustc_query_impl::execution::try_execute_query::<rustc_middle::query::caches::SingleCache<rustc_middle::query::erase::ErasedData<[u8; 0]>>, true>
  13: rustc_interface::interface::run_compiler::<(), rustc_driver_impl::run_compiler::{closure#0}>::{closure#2}
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Why it happens

The query is cacheable and reads the registry off the session. At dc3f8515:

rustc_middle/src/queries.rsarena_cache and desc, no eval_always:

query shallow_lint_levels_on(key: hir::OwnerId) -> &'tcx rustc_middle::lint::ShallowLintLevelMap {
    arena_cache
    desc { "looking up lint levels for `{}`", tcx.def_path_str(key) }
}

rustc_lint/src/levels.rs:

fn shallow_lint_levels_on(tcx: TyCtxt<'_>, owner: hir::OwnerId) -> ShallowLintLevelMap {
    let store = unerased_lint_store(tcx.sess);

rustc_lint/src/late.rs:

pub fn unerased_lint_store(sess: &Session) -> &LintStore {
    let store: &dyn Any = sess.lint_store.as_deref().unwrap();
    store.downcast_ref().unwrap()
}

rustc_session/src/session.rs — plain field, not Steal, not Untracked, not
a query:

/// This only ever stores a `LintStore` but we don't want a dependency on that type here.
pub lint_store: Option<Arc<dyn DynLintStore>>,

So the registered lint set is an input to a cached query with no dep-graph
representation, and nothing else differs between the runs: cargo passes
identical arguments, only the driver's environment changes. Hashing the
registered set into the incremental session fingerprint would fix it;
eval_always on the query would too but costs more, and this only arises under
a custom driver.

Notes

Which run trips varies. Two clean cycles of known → other → other → known gave
0,0,0,1 then 0,1,1,0. At least one ICE per registry change, never while the
set is held constant — probably because an ICEing run doesn't save its session.

CARGO_INCREMENTAL=0 avoids it.

Stock tooling doesn't reproduce it. Since clippy and rustdoc register lints
rustc doesn't, I expected cargo checkcargo clippy and cargo check
cargo doc over a shared target dir to be the same bug. Both stay clean on
1.94.0, with #[expect(dead_code)] and with #[expect(clippy::unreadable_literal)].

Related

#144271, #157495 and #144302 hit the same query. All three turn on #[expect]
and change source between runs; this has expectations: [] and identical
source, hence a separate issue. #154878 is a different, already-closed ICE.

AI Disclosure

This issue was found with the help of Claude during the development of one of my personal projects. The MWE and the bulk of the text on this issue were created with the help of claude as well. Happy to clarify or edit for brevity if needed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the ICE with the pinned cargo-dylint 6.0.1 sequence, then read the shallow_lint_levels_on query in rustc_middle/src/queries.rs and its implementation in rustc_lint/src/levels.rs, including the lint store access in rustc_lint/src/late.rs. Done means incremental compilation remains stable when the registered lint set changes, with regression coverage for the reproducer.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.