Skip to content

Agent-mode NuGet apply patches ~/.nuget/packages instead of the project's configured globalPackagesFolder / RestorePackagesPath, reports success, and VEX attests not_affected #397

Description

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

Summary

When a project moves its NuGet global packages folder, either with <add key="globalPackagesFolder" value=".nuget/packages" /> in nuget.config or with <RestorePackagesPath> in Directory.Build.props / the csproj, agent-mode apply patches the wrong copy:

  • Default cache is warm (any dev machine or CI image that has restored the same package id/version for another project): apply patches the user-wide ~/.nuget/packages/<id>/<ver>/ (%USERPROFILE%\.nuget\packages on Windows) and reports 1 of 1 targeted patch applied, exit 0. The project's own folder, the only one dotnet build resolves from (obj/project.assets.json → packageFolders), stays unpatched. socket-patch vex then emits not_affected / inline_mitigations_already_exist for the unpatched product. As a side effect, every other project on the machine that uses the default folder gets its bytes modified.
  • Default cache is cold and the project sits two directory levels below the scan root (the common App.sln + src/App/App.csproj layout): apply fails with "matched no installed package" (exit 1), because discover_paths_from_assets reads obj/project.assets.json only at the root and one level down.

Root cause (suspected)

NuGetCrawler::get_nuget_package_paths (crates/socket-patch-core/src/crawlers/nuget_crawler.rs:74-86) lists source roots in the order <cwd>/packages, then nuget_home() (default ~/.nuget/packages, only NUGET_PACKAGES honoured), then the packageFolders read from obj/project.assets.json. The dispatcher keeps only the first match per PURL (merge_first_wins, crates/socket-patch-cli/src/ecosystem_dispatch.rs:147, deliberate for NuGet). So whenever the default folder also holds the package, the project's real, configured folder is never patched. Separately, discover_paths_from_assets (nuget_crawler.rs:483) searches only one level deep, so a src/<Project>/obj/project.assets.json is never seen. Neither globalPackagesFolder nor RestorePackagesPath is read anywhere.

Impact

A silent false "patched", plus a false VEX not_affected attestation, for any repo that pins a repo-local packages folder. That's a common hermetic-CI pattern. Agent mode also mutates a shared user-wide cache the project doesn't use.

Repro (Linux, dotnet SDK 8.0.131, main f6b7fb9)

SP=/path/to/socket-patch
# 1. warm the default cache with the same package, as any other project would
mkdir warm && cd warm && cat > W.csproj <<'X'
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Newtonsoft.Json" Version="13.0.3" /></ItemGroup></Project>
X
dotnet restore && cd ..
# 2. project that relocates its packages folder
mkdir -p proj/src/App && cd proj
cat > nuget.config <<'X'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <config><add key="globalPackagesFolder" value=".nuget/packages" /></config>
</configuration>
X
cat > src/App/App.csproj <<'X'
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Newtonsoft.Json" Version="13.0.3" /></ItemGroup></Project>
X
echo 'System.Console.WriteLine(1);' > src/App/Program.cs
dotnet new sln -n App && dotnet sln add src/App/App.csproj && dotnet restore
# 3. stage a manifest patching LICENSE.md (sha256 git-blob hashes; blob in .socket/blobs/<afterHash>),
#    plus "setup": {"manual": ["nuget"]} so vex runs
$SP apply --offline         # -> "1 of 1 targeted patch applied", exit 0
grep -c MARKER .nuget/packages/newtonsoft.json/13.0.3/LICENSE.md     # 0  (the copy the build uses)
grep -c MARKER ~/.nuget/packages/newtonsoft.json/13.0.3/LICENSE.md   # 1  (wrong copy)
$SP vex --offline --product pkg:nuget/App@1.0.0   # status not_affected

RestorePackagesPath behaves the same: Directory.Build.props with <RestorePackagesPath>$(MSBuildThisFileDirectory)pkgs</RestorePackagesPath> and a root-level csproj. NuGetPackageRoot = <repo>/pkgs, yet apply patches ~/.nuget/packages.

With a cold default cache and the src/App layout, apply exits 1 with The targeted manifest patch matched no installed package. With the project one level down (App/App.csproj) and a cold default cache, it correctly patches .nuget/packages.

Expected vs actual

  • Expected: agent apply patches the copy NuGet actually resolves for the project, which is the packageFolders recorded in obj/project.assets.json / the configured globalPackagesFolder / RestorePackagesPath. Failing that, it refuses. README's apply section and the VEX contract (a statement only for a patch that is actually applied) both require this. docs/ecosystems.md documents NuGet agent mode as in-place patching of the installed package, with no caveat about relocated package folders.
  • Actual: it patches an unrelated user-wide cache, reports success, and VEX attests not_affected.

OS × SDK matrix (probe run https://github.com/SocketDev/socket-patch/actions/runs/36791969674)

OS SDK root-level project src/App project
Linux (local) 8.0.131 wrong copy patched, rc 0 wrong copy patched, rc 0 (cold default cache: not found, rc 1)
Linux 6.0.428 wrong copy patched, VEX not_affected same
Linux 9.0.318 wrong copy patched, VEX not_affected same
Linux 10.0.401 wrong copy patched, VEX not_affected same
macOS 8.0.425 wrong copy patched, VEX not_affected same
macOS 9.0.318 wrong copy patched, VEX not_affected same
Windows 8.0.425 wrong copy patched, VEX not_affected same
Windows 9.0.318 wrong copy patched, VEX not_affected same

Control: an App/ project one level deep with a cold default cache → project copy patched (pass).

First bad version

Not a regression: v4.0.0 (GitHub release binary) behaves identically.

Related but distinct: #338 (the same "wrong copy, success, VEX not_affected" shape for cargo vendor dirs), #352 (the vendored/hosted warm-cache shadowing).

Activity

  1. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Shares root cause with #398: NuGetCrawler::get_nuget_package_paths (crates/socket-patch-core/src/crawlers/nuget_crawler.rs:68-86) hard-codes the package folders (<cwd>/packages, then ~/.nuget/packages / NUGET_PACKAGES) and never resolves the folder NuGet is actually configured to use (globalPackagesFolder / repositoryPath in nuget.config, RestorePackagesPath), so the default cache wins under first-match-per-PURL. Will be fixed together.

    Triage: priority:p3 (NuGet). Not a duplicate of #352 (that one is about vendored/hosted builds being shadowed by a warm cache, a different code path).


    Generated by Claude Code

  2. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triage on main 2463257 (v5 consolidation, #277): still reproduces. The original src/App + project nuget.config globalPackagesFolder repro patches ~/.nuget/packages (rc 0) and leaves .nuget/packages unpatched, and vex says not_affected.

    New information: the same root cause also breaks global mode (-g). When globalPackagesFolder is set in the user-level config (~/.nuget/NuGet/NuGet.Config, %APPDATA%\NuGet\NuGet.Config), dotnet nuget locals global-packages -l and every restore use that folder, but:

    • scan -g crawls only ~/.nuget/packages (stale). Packages that exist only in the real folder (here humanizer.core@2.14.1) are never sent to the patch API or listed.
    • apply -g patches the ~/.nuget/packages copy (Using NuGet packages at: ~/.nuget/packages, rc 0). The copy NuGet actually uses stays unpatched.
    • vex -g attests not_affected for it.
    cat > ~/.nuget/NuGet/NuGet.Config <<'X'
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <packageSources><add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /></packageSources>
      <config><add key="globalPackagesFolder" value="/tmp/gpf" /></config>
    </configuration>
    X
    dotnet nuget locals global-packages -l    # global-packages: /tmp/gpf
    # restore a project with Newtonsoft.Json 13.0.3 + Humanizer.Core 2.14.1 (default cache already holds Newtonsoft only)
    # stage a manifest for pkg:nuget/newtonsoft.json@13.0.3 (LICENSE.md), then from a non-project dir:
    socket-patch apply -g --offline   # rc 0, "applied"
    grep -c SOCKET_MARKER /tmp/gpf/newtonsoft.json/13.0.3/LICENSE.md               # 0
    grep -c SOCKET_MARKER ~/.nuget/packages/newtonsoft.json/13.0.3/LICENSE.md      # 1
    socket-patch vex -g --offline --product pkg:nuget/x@1.0.0   # status not_affected

    Reproduced 2/2 locally (Linux, SDK 8.0.131) and in 8/8 probe cells, run https://github.com/SocketDev/socket-patch/actions/runs/36820498426: ubuntu 6.0.x / 9.0.x / 10.0.x, macos 8.0.x / 9.0.x, windows 8.0.x / 9.0.x / 10.0.x. Every cell: scan -g misses the configured folder, apply -g patches the default folder, VEX not_affected.

    The code path is nuget_crawler.rs:41-50 (the global branch returns nuget_home(), which honours only NUGET_PACKAGES). A fix for this issue should read the effective globalPackagesFolder in global mode too.


    Generated by Claude Code

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