Skip to content

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 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

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