[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
Hosted and vendored NuGet patches keep the package's upstream id and version: the Socket source (socket-patch-<uuid> feed or the committed .socket/vendor/nuget/<uuid>/ folder feed) serves a patched Newtonsoft.Json 13.0.3. NuGet checks the global packages folder (NUGET_PACKAGES / ~/.nuget/packages) before any source. If that folder already holds newtonsoft.json/13.0.3/ from nuget.org, the source mapping is never consulted. That is always true on the machine that ran socket-patch (vendored mode even rebuilds the nupkg from that cached copy), and it's common on dev machines and CI runners with a restored NuGet cache. The result:
- No
packages.lock.json: dotnet restore and dotnet build succeed silently against the unpatched cached bytes. The Socket feed gets zero requests. vex still attests the package not_affected. Its only warning tells the user to "re-run your package manager's install to resync it", which doesn't help, because restore keeps the cached copy.
- With
packages.lock.json: every dotnet restore / dotnet build on that machine fails right after vendoring or redirecting with NU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3, until the user deletes the cache entry. socket-patch never mentions this.
Neither vendor / scan --mode vendored nor scan --mode hosted warns about it. The vendor_nuget_no_lockfile warning actively says the opposite: "the vendored feed forces Newtonsoft.Json from the patched copy".
Impact
- Silent unpatched builds, plus a false VEX
not_affected, for lockfile-less projects. That's the default: RestorePackagesWithLockFile is off unless opted in.
- Guaranteed NU1403 breakage right after patching for locked projects, on the developer's machine and on any CI runner whose NuGet cache is restored by a fallback key such as
restore-keys: nuget-.
Repro (vendored, real dotnet 8.0.131 on Linux, main f6b7fb9)
export NUGET_PACKAGES=$PWD/cache SOCKET_OFFLINE=1
mkdir app && cd app
cat > app.csproj <<'E'
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework>
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile></PropertyGroup>
<ItemGroup><PackageReference Include="Newtonsoft.Json" Version="13.0.3" /></ItemGroup></Project>
E
echo 'System.Console.WriteLine("ok");' > Program.cs
dotnet restore
# stage a marker patch on LICENSE.md (.socket/manifest.json + blob, same shape as
# crates/socket-patch-cli/tests/docker_vendor_common stage_patch)
socket-patch vendor --json --offline # status: success (+ vendor_nuget_no_lockfile)
rm -rf obj bin && dotnet restore --force && dotnet build # succeeds
grep -c SOCKET-MARKER $NUGET_PACKAGES/newtonsoft.json/13.0.3/LICENSE.md # 0 -> unpatched
NUGET_PACKAGES=$PWD/../cold dotnet restore # control
grep -c SOCKET-MARKER ../cold/newtonsoft.json/13.0.3/LICENSE.md # 1 -> patched
socket-patch vex --product pkg:nuget/app@1.0.0 --output out.vex.json # not_affected
With RestorePackagesWithLockFile=true, dotnet restore --locked-mode (and a plain dotnet build) on the same cache fails NU1403 right after vendor reports success.
Hosted: I reproduced it with the repo's own e2e_nuget_dotnet_build.rs wiremock patch-server stand-in, adding a local scratch test. After scan --mode hosted (redirected: 1, warnings: []), a warm-cache dotnet restore --locked-mode fails NU1403. With the lock removed, restore succeeds with the pristine LICENSE.md and 0 hits on the Socket feed's .nupkg.
Expected vs actual
- Expected: docs/ecosystems.md ("NuGet locked mode") says "the feed + source mapping still force the patched copy", and the
vendor_nuget_no_lockfile detail says the same. CLI_CONTRACT.md's vendored table names "dotnet restore --locked-mode, cold cache" as the verification, and the docs don't mention the warm-cache precondition anywhere. The Maven twin of this problem is documented and warned (vendor_maven_local_cache_shadow, "Warm ~/.m2 shadowing"). NuGet has neither.
- Actual: the warm global packages folder shadows the feed, silently without a lock, and with NU1403 with a lock.
At minimum this needs a vendor_nuget_global_cache_shadow-style warning (and a hosted equivalent) naming the fix, e.g. rm -rf ~/.nuget/packages/<idLower>/<version> or dotnet nuget locals global-packages --clear. Evicting the stale <idLower>/<version> folder, or a unique version the way Maven is suffixed, would fix it for real.
OS × version
| cell |
no lock (warm) |
lock, --locked-mode (warm) |
cold cache (control) |
| Linux, SDK 8.0.131, vendored, main f6b7fb9 |
unpatched, silent ×2 |
NU1403 ×2 |
patched |
| Linux, SDK 8.0.131, hosted (wiremock stand-in), main |
unpatched, silent ×2 |
NU1403 ×2 |
patched (existing suite) |
| Linux, SDK 8.0.131, vendored, v4.0.0 |
unpatched, silent |
— |
— |
| probe: vendored, main f6b7fb9: ubuntu SDK 6.0.428 / 7.0.410 / 8.0.425 / 9.0.318 / 10.0.401, macOS 8.0.425 / 9.0.318, Windows 8.0.425 / 9.0.318 |
unpatched, silent (all 9) |
NU1403 (all 9) |
patched (all 9) |
First bad release: not a regression. v4.0.0 already behaves this way.
Suspect code
crates/socket-patch-core/src/vendor/nuget_feed.rs:653: the vendor_nuget_no_lockfile text, and there's no global-packages-folder check anywhere in the backend.
crates/socket-patch-core/src/patch/redirect/mod.rs:5337 (rewrite_nuget): it keeps resolved at the upstream version, so any cached <id>/<version> shadows the Socket source.
docs/ecosystems.md:383: the "NuGet locked mode" caveat.
Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36755202463
[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
Hosted and vendored NuGet patches keep the package's upstream id and version: the Socket source (
socket-patch-<uuid>feed or the committed.socket/vendor/nuget/<uuid>/folder feed) serves a patchedNewtonsoft.Json 13.0.3. NuGet checks the global packages folder (NUGET_PACKAGES/~/.nuget/packages) before any source. If that folder already holdsnewtonsoft.json/13.0.3/from nuget.org, the source mapping is never consulted. That is always true on the machine that ransocket-patch(vendored mode even rebuilds the nupkg from that cached copy), and it's common on dev machines and CI runners with a restored NuGet cache. The result:packages.lock.json:dotnet restoreanddotnet buildsucceed silently against the unpatched cached bytes. The Socket feed gets zero requests.vexstill attests the packagenot_affected. Its only warning tells the user to "re-run your package manager's install to resync it", which doesn't help, because restore keeps the cached copy.packages.lock.json: everydotnet restore/dotnet buildon that machine fails right after vendoring or redirecting withNU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3, until the user deletes the cache entry. socket-patch never mentions this.Neither
vendor/scan --mode vendorednorscan --mode hostedwarns about it. Thevendor_nuget_no_lockfilewarning actively says the opposite: "the vendored feed forces Newtonsoft.Json from the patched copy".Impact
not_affected, for lockfile-less projects. That's the default:RestorePackagesWithLockFileis off unless opted in.restore-keys: nuget-.Repro (vendored, real dotnet 8.0.131 on Linux, main f6b7fb9)
With
RestorePackagesWithLockFile=true,dotnet restore --locked-mode(and a plaindotnet build) on the same cache failsNU1403right aftervendorreports success.Hosted: I reproduced it with the repo's own
e2e_nuget_dotnet_build.rswiremock patch-server stand-in, adding a local scratch test. Afterscan --mode hosted(redirected: 1,warnings: []), a warm-cachedotnet restore --locked-modefails NU1403. With the lock removed, restore succeeds with the pristine LICENSE.md and 0 hits on the Socket feed's.nupkg.Expected vs actual
vendor_nuget_no_lockfiledetail says the same. CLI_CONTRACT.md's vendored table names "dotnet restore --locked-mode, cold cache" as the verification, and the docs don't mention the warm-cache precondition anywhere. The Maven twin of this problem is documented and warned (vendor_maven_local_cache_shadow, "Warm~/.m2shadowing"). NuGet has neither.At minimum this needs a
vendor_nuget_global_cache_shadow-style warning (and a hosted equivalent) naming the fix, e.g.rm -rf ~/.nuget/packages/<idLower>/<version>ordotnet nuget locals global-packages --clear. Evicting the stale<idLower>/<version>folder, or a unique version the way Maven is suffixed, would fix it for real.OS × version
--locked-mode(warm)First bad release: not a regression. v4.0.0 already behaves this way.
Suspect code
crates/socket-patch-core/src/vendor/nuget_feed.rs:653: thevendor_nuget_no_lockfiletext, and there's no global-packages-folder check anywhere in the backend.crates/socket-patch-core/src/patch/redirect/mod.rs:5337(rewrite_nuget): it keepsresolvedat the upstream version, so any cached<id>/<version>shadows the Socket source.docs/ecosystems.md:383: the "NuGet locked mode" caveat.Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36755202463