Repository navigation
Vendored and hosted NuGet patches are shadowed by a warm global packages folder: silently unpatched without a lock, NU1403 with one #352
Description
[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 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. - With
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 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:RestorePackagesWithLockFileis 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_affectedWith 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_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. - 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: 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
Activity
mikolalysenko commentedon Sep 30, 2026
[agent] Triaged:
priority:p3(NuGet). This isn't a duplicate, and I found no existing fix PR. The cause is that patches keep the upstream id and version, so a warm global packages folder wins over the Socket source. It's related to #353 and #354, but they have separate causes. The fix may need a design decision: a distinct version, a per-projectglobalPackagesFolder, or a loud warning.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Re-checked on main
2463257(the v5 consolidation, #277): still reproduces in both hosted and vendored (--vendor-source service) mode on Linux, SDK 8.0.131.I used the repo's real-
dotnetharness (e2e_nuget_dotnet_build.rs:Backendstand-in, real nuget.org fixture restore). Right afterscan --mode hosted|vendored(exit 0,warnings: []), I restored the same checkout against the same, warmNUGET_PACKAGES:dotnet restore --locked-mode→error NU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3. The package is different than the last restore., and the cached LICENSE.md is still pristine.dotnet restoregives the same NU1403.The scan envelope still has no warning or remedy for the warm cache (for example
dotnet nuget locals global-packages --clear, or removing~/.nuget/packages/<id>/<ver>), unlike the Pipenv/gem stale-install guards in CLI_CONTRACT.md. A cold-cache fresh checkout still passes, as the shipped e2e asserts.Generated by Claude Code
nuget: run ledger (#462, #352/#426 comments)
mikolalysenko commentedon Oct 9, 2026
v5 release blocker (P1). A normal warm NuGet cache defeats the first hosted/vendored patch. The documented install step must actually consume the patch, with a working cache remedy if needed.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[agent] Claiming for v5 blocker burn-down (shared root cause: patched NuGet packages keep upstream id+version, so a warm global packages folder entry shadows the Socket source). Branch: agent/v5-nuget-gpf-shadow. Claim-ID: 2026-10-09T16:44Z-762cf2
Report NuGet patches shadowed by a warm cache
Merge main into agent/v5-nuget-gpf-shadow
Merge main into agent/v5-nuget-gpf-shadow
Merge main into v5/json-envelope-unify