Skip to content

Hosted NuGet remove / rollback / takeover restore packages.lock.json to the nuget.org catalog packageHash, which isn't NuGet's contentHash for signed packages, so every later dotnet restore fails NU1403 #624

Description

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

Summary

The v5.0 hosted unwind (remove, rollback, and the hosted → vendored takeover that vendor --revert later lands on) restores every packages.lock.json entry of the patched id to "nuget.org's contentHash". It reads that value from the nuget.org catalog packageHash (UpstreamClient::nuget_content_hash, crates/socket-patch-core/src/patch/redirect/upstream/client.rs:498). The two hashes are different things:

  • The catalog packageHash is the SHA-512 of the whole .nupkg file as served, including .signature.p7s.
  • NuGet's lock contentHash (and .nupkg.metadata) is the package's content hash. For a signed package, NuGet computes it with the signature excluded (PackageArchiveReader.GetContentHash → SignedPackageArchiveUtility).

nuget.org has repository-signed every package since 2018, so for practically every real package the restored lock pins a hash that no download will ever match. Every later dotnet restore, locked or not, fails NU1403: Package content hash validation failed … The package is different than the last restore.

Live values for Newtonsoft.Json 13.0.3:

value hash
lock contentHash written by dotnet restore (correct, original) HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==
catalog packageHash (catalog0/data/2023.03.08.07.46.17/newtonsoft.json.13.0.3.json) mbJSvHfRxfX3tR/U6n1WU+mWHXswYc+SB/hkOpx8yZZe68hNZGfymJu0cjsaJEkVzCMqePiU6LdIyogqfIn7kg==
sha512(base64) of the flat-container .nupkg file mbJSvH…kg== (equals the catalog value)
lock after socket-patch remove / rollback mbJSvH…kg== ❌

The unit tests in upstream/nuget.rs mock the catalog with the same UPSTREAM hash the lock fixture uses, so they can't catch this mismatch.

Impact

Undoing a hosted NuGet patch breaks the project's restore: the command reports success / hosted_reverted, nuget.config comes back byte-exact, and then dotnet restore fails NU1403 on every machine until someone deletes or regenerates the lock by hand. The same bad hash becomes the vendor ledger's recorded original on a hosted → vendored takeover, so vendor --revert later writes it back too.

Repro (Linux, .NET SDK 8.0.131, main 045d7ec, real api.nuget.org)

This uses the Backend stand-in and fixture from crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs: app.csproj with RestorePackagesWithLockFile, a nuget.org-only nuget.config, and a real dotnet restore of Newtonsoft.Json 13.0.3.

cp packages.lock.json lock.orig
socket-patch scan --mode hosted --json --yes --api-url $URI --org test-org \
  --api-token fake-token --patch-server-url $URI           # redirected: 1
# lockfile discovery only recognises production feed URLs, so point the
# stand-in's source at patch.socket.dev (what a real hosted run writes):
sed -i "s#$URI#https://patch.socket.dev#" nuget.config

socket-patch remove pkg:nuget/newtonsoft.json@13.0.3 --json
# status success, events: [{"action":"removed","errorCode":"hosted_reverted",
#   "reason":"hosted lockfile pin restored to the upstream registry on remove"}]
cmp nuget.config <original config>     # byte-exact
diff lock.orig packages.lock.json
# <  "contentHash": "HrC5BXdl…a+zQ=="
# >  "contentHash": "mbJSvHfR…n7kg=="
# (and a trailing newline is added)

# fresh checkout, cold global packages folder:
dotnet restore                 # rc 1: error NU1403 … Newtonsoft.Json.13.0.3
dotnet restore --locked-mode   # rc 1: error NU1403

The same result (lock = mbJSv…, NU1403) also comes from:

  • rollback pkg:nuget/newtonsoft.json@13.0.3: hosted.reverted: [purl], editedFiles: 2, status: success.
  • hosted project → vendor --vendor-source service … (takeover: lock re-pinned to the vendored nupkg) → vendor --revert: status: success, nuget.config unwired, lock = mbJSv….

Reproduced 5 times (remove with an LF, a CRLF and a BOM nuget.config; rollback; takeover → vendor --revert).

Expected vs actual

  • Expected: CLI_CONTRACT.md says remove "a hosted-only match restores its upstream registry entry", and "Takeover reconciliation" says "vendor --revert lands back on upstream registry state". SOCKET_NUGET_URL documents that the restored contentHash "comes from" the catalog packageHash, but the restored entry has to be one NuGet accepts: the original contentHash, which dotnet restore would write.
  • Actual: a hash NuGet never produces for a signed package, so restore fails NU1403.

Possible fixes: compute the content hash the way NuGet does (download the nupkg, drop .signature.p7s per SignedPackageArchiveUtility, then hash). Or, when the catalog says the package is signed, refuse with redirect_revert_failed and point to git checkout -- packages.lock.json instead of writing a wrong pin.

Matrix

OS SDK path result
Linux 8.0.131 hosted remove (LF / CRLF / BOM config) fail NU1403
Linux 8.0.131 hosted rollback <purl> fail NU1403 (locked and plain restore)
Linux 8.0.131 hosted → vendor takeover → vendor --revert fail (lock = catalog hash)
macOS / Windows, SDK 6/9/10 — — not run. The hash comes from nuget.org and NuGet's content-hash algorithm is the same on every OS and SDK, so the mismatch is platform-independent.

First bad version

Release v4.0.0 isn't affected: there, remove / rollback on a manifest-less hosted project exits manifest_not_found, so there's no hosted unwind. The NuGet unwind arrived with de316b4 (#358, 2026-10-01), which added crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/upstream/client.rs:498-552: nuget_content_hash returns the catalog packageHash.
  • crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs:301: writes that value into every lock entry of the id.
  • crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs:13-14 (module doc), and SOCKET_NUGET_URL in CLI_CONTRACT.md:1040, which describe the same assumption.

Activity

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:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:nugetNuGet / dotnetpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions