Skip to content

Vendored NuGet on a core.autocrlf checkout: vendor --revert / remove / rollback revert packages.lock.json but leave nuget.config wired, so every restore fails NU1403 (vendor --revert exits 0) #537

Description

[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).

Summary

Vendored NuGet writes nuget.config with LF line endings and records the exact text it wrote. When the project is committed and then checked out with core.autocrlf=true (Git for Windows' default, and common on any OS), nuget.config and packages.lock.json come back with CRLF. On that checkout, every revert path ends up half-done:

  1. The lock record is reverted first (revert_nuget_opts walks the wiring in reverse order). It's a JSON string swap, so CRLF doesn't affect it, and packages.lock.json goes back to the upstream contentHash.
  2. Then the config record is reverted. The whole-file fast path compares the live text with the recorded LF text, and the fallback excision looks for LF-terminated fragments (" <add key=\"…\" value=\"…\" />\n" and " <packageSource key=\"…\">\n"). Neither matches CRLF, so the config is treated as drifted and left wired (vendor_lock_entry_drifted), and the artifact is kept.

The result is a project whose lock pins upstream bytes while nuget.config still maps Newtonsoft.Json exclusively to the vendored feed. Every later dotnet restore, locked or not, fails with NU1403: Package content hash validation failed.

  • vendor --revert exits 0 with status: success.
  • remove <purl> exits 1 and says "every matching entry's vendored state drift-kept; nothing was removed". But it did rewrite packages.lock.json.
  • rollback exits 1 (partial_failure), and it also rewrites the lock.
  • The suggested remedy doesn't help. Re-running scan --mode vendored, as vendor_revert_kept advises, reports already_vendored / "artifact and lockfile wiring already in sync", and the next vendor --revert half-reverts again.
  • vendor --check on the same checkout reports vendor_check_ok.

Impact

A Windows developer, or anyone using autocrlf=true, who clones a vendored NuGet project and tries to un-vendor it gets a project that no longer restores. vendor --revert reports success while doing it. The only way out is to fix the lock or config by hand, or to git checkout both files. There's nothing for the user to "undo", because the only drift is git's line-ending conversion of a file socket-patch itself committed.

Repro (Linux / macOS / Windows, any SDK)

I drove this with a scratch copy of crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs: the same wiremock Backend stand-in, a real nuget.org fixture restore, and the real dotnet.

# app.csproj: net8.0, RestorePackagesWithLockFile=true, PackageReference Newtonsoft.Json 13.0.3
# nuget.config: nuget.org only (LF)
dotnet restore
socket-patch scan --mode vendored --vendor-source service --json --yes ...   # rc 0; lock re-pinned, feed + mapping wired
git init && git add -A && git commit -m vendored

git clone -c core.autocrlf=true <repo> wc && cd wc      # what Git for Windows does by default
file nuget.config                                         # "... with CRLF line terminators"
dotnet restore --locked-mode                              # rc 0, PATCHED LICENSE.md installed (baseline ok)

socket-patch vendor --revert --json
#   rc 0, "status": "success"
#   events: skipped vendor_lock_entry_drifted "nuget.config no longer carries what vendor wrote for socket-patch-<uuid>; left alone"
#           skipped vendor_artifact_kept, skipped vendor_revert_kept
git status --short        #  M packages.lock.json      (lock reverted to the upstream contentHash)
grep -c socket-patch nuget.config                         # 2 (source + mapping still there)
rm -rf obj && dotnet restore --locked-mode                # rc 1: error NU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3
dotnet restore                                            # rc 1: same NU1403

remove pkg:nuget/Newtonsoft.Json@13.0.3 (rc 1, vendor_revert_kept "nothing was removed") and rollback (rc 1) leave exactly the same state: lock modified, config wired, NU1403. Running scan --mode vendored first and then vendor --revert behaves the same way.

Control: on the same commit cloned without autocrlf, vendor --revert restores both files byte-for-byte and the locked restore installs the pristine package.

Expected vs actual

  • Expected: CLI_CONTRACT.md says vendor --revert "restores the originals". Drift-keep is for fragments "that no longer match — a user re-resolved", and it should keep the project restorable. The contract already treats a checkout's line-ending conversion as something revert must survive for cargo ("remove / rollback match the recorded fragments across a later CRLF↔LF checkout conversion"). The minimum is that a revert which keeps the config wired must not revert the lock pin on its own, and must not report success / "nothing was removed".
  • Actual: a git line-ending conversion counts as drift. The lock is reverted anyway, the config isn't, and the project can't restore.

OS × version (probe run, all with git clone -c core.autocrlf=true)

OS SDK vendor --revert remove rollback rescan → revert
Linux (sandbox) 8.0.131 repro (rc 0) repro (rc 1) repro (rc 1) repro
ubuntu-latest 8.0.x, 10.0.x repro repro repro repro
macos-latest 8.0.x repro repro repro repro
macos-latest 10.0.x job ran; I only inspected the named-lock cells in its log
windows-latest (Git for Windows, system autocrlf=true) 8.0.x, 10.0.x repro repro repro repro
any any, no autocrlf (control) pass pass pass pass

Reproduced 3× in the sandbox on main 61cfb9b, and on every probe cell: https://github.com/SocketDev/socket-patch/actions/runs/36970426074

Not bisected. The LF-anchored fragments in the NuGet revert look as old as the vendored NuGet backend.

Suspect code

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p3 (NuGet). Not a duplicate, and no PR fixes it yet.

    This is the NuGet counterpart of the Gradle autocrlf issue #429, but the code paths differ: here it's the LF-anchored whole-file and fragment matches in vendor/nuget_feed.rs (around :1180, :1206, :1236), plus the reverse-order loop at :728 that reverts the lock record even when the config record is drift-kept. Both need a fix: line-ending-tolerant matching, and an all-or-nothing revert per purl.


    Generated by Claude Code

  2. added
    v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.
    compatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.
    and removed on Oct 9, 2026
  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 release blocker (P1). Routine vendored NuGet undo must survive a normal Windows core.autocrlf checkout and restore both the lock and nuget.config together.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  4. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming for v5 blocker burn-down (shared root cause: vendored nuget.config revert matches LF-anchored fragments and reverts the lock before confirming the config revert). Branch: agent/v5-nuget-crlf-revert. Claim-ID: 2026-10-09T16:44Z-9319c8

  5. added a commit that references this issue on Oct 9, 2026
    fb711df
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:claimedagent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentcompatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.pm:nugetNuGet / dotnetpriority:p1v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions