rust-lang / rust-lang/rust-analyzer

flycheck's `cargo check` bypasses the lockfile redirection the build-script check receives, rewriting the project's Cargo.lock

Open
#23,243 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
16.9k
Forks
2.2k
Avg merge
1d 12h
Merged PRs (30d)
72

Description

Opening a project in rust-analyzer rewrites a tracked Cargo.lock when a resolve has work to do — for example when a path dependency on disk is newer than what the lockfile records.

rust-analyzer already protects against this: during workspace load it runs cargo check for build scripts with CARGO_RESOLVER_LOCKFILE_PATH pointing at a temp copy, so that resolve cannot touch the real lockfile. The flycheck cargo check does not get that treatment, and its resolve writes the project's lockfile directly.

So the two cargo check invocations in a single session disagree about whether the lockfile is writable.

Reproduction

Four files, no network, path dependency only.

mkdir -p /tmp/ra-minrepro/{dep/src,app/src}
cd /tmp/ra-minrepro

cat > dep/Cargo.toml <<'EOF'
[package]
name = "minrepro-dep"
version = "0.1.0"
edition = "2021"
EOF
echo 'pub fn hello() -> &'"'"'static str { "hi" }' > dep/src/lib.rs

cat > app/Cargo.toml <<'EOF'
[package]
name = "minrepro-app"
version = "0.1.0"
edition = "2021"

[dependencies]
minrepro-dep = { path = "../dep" }
EOF
echo 'pub fn greet() -> &'"'"'static str { minrepro_dep::hello() }' > app/src/lib.rs

# generate the lockfile against dep 0.1.0, then bump the dep
( cd app && cargo generate-lockfile )
sed -i 's/^version = "0.1.0"/version = "0.1.1"/' dep/Cargo.toml

sha256sum app/Cargo.lock          # record this

Now open /tmp/ra-minrepro/app in an editor with rust-analyzer, or drive the server over stdio with initialize + initialized against rootUri: file:///tmp/ra-minrepro/app and wait for workspace load.

sha256sum app/Cargo.lock          # changed
grep -A1 'name = "minrepro-dep"' app/Cargo.lock   # now 0.1.1

Observed here: fdff43a8…ab606929…, minrepro-dep 0.1.00.1.1. No file was opened and nothing was saved; workspace load alone is enough.

Mechanism

Both cargo check invocations from one session, captured with a logging cargo shim recording argv and environment per invocation:

check #1 — build scripts, protected

check --quiet --workspace --message-format=json --manifest-path <root>/Cargo.toml \
      -Zlockfile-path --keep-going --compile-time-deps --all-targets -Zunstable-options
CARGO_RESOLVER_LOCKFILE_PATH=/tmp/rust-analyzer3dbd91-1/Cargo.lock
__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS=nightly

check #2 — flycheck, writes the lockfile

check --workspace --message-format=json --manifest-path <root>/Cargo.toml \
      --keep-going --all-targets
(neither variable set)

The redirection is the environment variable, not the flag. Running check #2's argv verbatim:

  • without those variables → the source lockfile is rewritten (fdff43a8…ab606929…)
  • with CARGO_RESOLVER_LOCKFILE_PATH and the channel override exported, nothing else changed → the source lockfile is untouched and the redirect target receives the write instead

Same resolution, different destination, one variable.

Note that -Zlockfile-path in check #1's argv is inert: cargo 1.97 stabilized it, and cargo emits flag -Z lockfile-path has been stabilized in the 1.97 release, and is no longer necessary. It names no path. We initially believed the flag was the discriminator; the A/B above disproved that.

Two things a reader may reasonably ask

Should flycheck use a redirected lockfile at all? The precedent is already in-tree — the build-script check resolves against a redirected lockfile today. This asks for consistency with existing behaviour rather than new policy.

Does redirecting change diagnostics? Not in the case we measured. Running check #2's argv against the same tree with and without CARGO_RESOLVER_LOCKFILE_PATH set, on a version of the repro that emits diagnostics (an unused variable, so the comparison is non-empty and could fail), both runs emitted the same two compiler-message entries with byte-identical rendered output. The redirect changes where the resolution is written, not what resolves.

Environment and limits

rust-analyzer 0.3.2963-standalone
cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1.1)
rustc 1.98.0 (88d9e12ae 2026-08-18)
CachyOS (Arch-derived), kernel 7.2.0-1-cachyos

Stating the limits of what we verified, since some of this reads stronger than it is:

  • The rust-analyzer behaviour was observed on one host and one rust-analyzer build (0.3.2963). Four instruments were used — two independently written LSP clients, a logging cargo shim, and a /proc poller — and the behaviour was checked twice by a second party, on a separately written LSP client. First, from a prose description of the setup only, building an equivalent tree with a different layout and different file contents: same behaviour, different hashes — which rules out a reproduction that only works for its author's exact files. Second, by running the block above verbatim: the resulting lockfile hashes were identical to the ones recorded here (fdff43a8…ab606929…), so the reproduction is deterministic as published. Neither establishes environment independence for the rust-analyzer half.
  • The cargo-side write was reproduced on a second machine: Ubuntu 24.04.4, glibc 2.39, cargo 1.94.1 (Homebrew) — a different distro, libc, cargo version and cargo packaging. Same transition. Note 1.94.1 predates the 1.97 stabilization of -Zlockfile-path, so the write behaviour is not tied to that stabilization.
  • No machine on which we ran a rust-analyzer session had rust-src installed, so rust-analyzer emitted can't load standard library from sysroot during those runs. Both checks fired and the write happened regardless. The second machine, where the cargo-side write reproduced, did have rust-src present — so sysroot availability does not prevent the write on the cargo side. We could not test a rust-analyzer session with rust-src present.

Why it matters

The write is invisible to the caller. A diagnostic action with read-only intent mutates a tracked file, and the trigger — a path dependency on a sibling checkout that has moved ahead of the committed lockfile — arises in any workflow where two related crates are developed side by side. Editors that load a workspace on open will do this without user action.

Related: #19011, #19729, #15165 and PR #20018, which introduced the lockfile copy for cargo metadata. This report is about the flycheck cargo check path, which that work does not cover.

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 at rust-analyzer's flycheck cargo check path and compare it with the existing build-script check that sets CARGO_RESOLVER_LOCKFILE_PATH. Trace how each invocation builds its environment, then run the four-file path-dependency reproduction and verify workspace loading leaves app/Cargo.lock unchanged while diagnostics remain equivalent.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.