Description
rust-analyzer version: 0.3.2441-standalone (d8887c0758 2025-04-26)
rustc version: `rustc 1.85.1 (4eb161250 2025-03-15)
editor or extension: Cursor v0.49.6 based on VSCode 1.96.2 + official rust-lang.rust-analyzer
extension version 0.3.2441
relevant settings: VSCode workspace settings file containing
"rust-analyzer.cargo.extraArgs": ["--frozen"],
"rust-analyzer.cargo.extraEnv": { "CARGO_NET_OFFLINE": "true" },
"rust-analyzer.cargo.targetDir": true,
What I expected to happen: r-a cannot cause Cargo.lock
changes, due to "rust-analyzer.cargo.extraArgs": ["--frozen"]
What actually happens: r-a occasionally causes Cargo.lock
changes, due to a cargo metadata
call that doesn't supply --frozen
How I verified this is from r-a and not something else:
- replaced
cargo
with a shim that logs full invocations - this recorded an r-a invocation of
cargo metadata --format-version 1 --manifest-path /path/to/Cargo.toml --filter-platform <my-platform>
— note this did not include the configured--frozen
- git then reported changes in
Cargo.lock
How this happens in the real world
In the real-world this happens due to a "torn read" race condition versus git checkout
. I'll describe the real-world situation first for intuition-building purposes, then offer a way to simulate the outcome of the race for easier debugging.
Consider a large repo where a git checkout
may take a noticeable amount of time. Say we move from a newer branch to an older one, where the newer branch had updated some dependencies and picked up new ones.
git checkout <older-branch>
- mid-checkout, imagine
Cargo.toml
becomes updated but the matchingCargo.lock
change isn't applied yet — git is busy updating other files and hasn't gotten to it yet - at this instant, the VSCode extension runs r-a
- r-a runs
cargo metadata
without--frozen
, despite the configuration (as mentioned above) - (not positive about this, but possible)
"rust-analyzer.cargo.targetDir": true
means the lockfile isn't locked during this process cargo metadata
readsCargo.toml
andCargo.lock
, and continues running (A)- while
cargo metadata
is in flight, the git checkout updatesCargo.lock
(B) cargo
notices that it's getting a different resolution: some of the dependencies present in theCargo.lock
it read at (A) are superfluous, so it rewritesCargo.lock
to exclude them — but it leaves the updated versions of other still-used dependencies (C)- (C) happens after (B), so git's checkout of
Cargo.lock
at (B) is overwritten by (C)
At this point, both the checkout and the r-a run are complete. A subsequent git status
shows Cargo.lock
has newer versions of some dependencies — a hybrid lockfile between the newer and older branch.
Simulating the torn read
The easiest way to simulate the race is:
git checkout -b older-branch
git checkout -b newer-branch
cargo update # to cause existing dependencies' versions to be updated to newer versions
cargo add <some-new-dependency>
git commit -m "lockfile bump"
# switch to the older branch but check out the newer branch's lockfile
git checkout older-branch
git checkout newer-branch Cargo.lock
# at this point, we've set up the "torn read" condition manually;
# cargo commands like `cargo check` or `cargo metadata` will update
# `Cargo.lock` to remove `<some-new-dependency>` from the lockfile
# but will *preserve* the `cargo update` outcome otherwise,
# creating a new `Cargo.lock` that is unlike either branch's contents
r-a origin of the cargo metadata
call
AFAICT the cargo metadata
invocation in question is coming from this block, which explicitly passes locked: false
. There's a similar block above it, for the rustc_dir
case when there's a custom sysroot, which might also be similarly affected — I'm not sure.
Happy to provide any more info or help however I can. Thanks for looking into this!