Skip to content

NuGet scan --mode vendored over a hosted pin skips the takeover (purl case mismatch), keeps the hosted feed wired, writes no vendor ledger and still reports success #553

Description

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

Summary

On a NuGet project that scan --mode hosted wired, a later scan --mode vendored never runs the documented hosted → vendored takeover. The hosted-pin lookup compares purls case-sensitively. Lockfile discovery reports the pin as pkg:nuget/newtonsoft.json@13.0.3 (lowercased id), but the crawler/API candidate is pkg:nuget/Newtonsoft.Json@13.0.3, so hosted_pin_of finds nothing.

The NuGet vendored backend then sees the socket-patch-<uuid> key that hosted mode wrote into nuget.config. Hosted and vendored use the same key, so it concludes the project is already vendor-wired. It takes the "artifact missing/stale" branch and rebuilds .socket/vendor/nuget/<uuid>/*.nupkg with "(nuget.config untouched)". The run exits 0, status: success, applied.

The result:

  • nuget.config still points the package at the hosted, grant-token-bearing patch.socket.dev/patch-registry/nuget/<token>/<uuid>/index.json feed. Restores keep downloading from it, so the project still needs that network access and the token URL, which is the reason to switch to vendored.
  • No vendor ledger (.socket/vendor/state.json) is written. list still reports the package as mode: hosted.
  • A stray nupkg is left for the user to commit. The next vendor --revert deletes it as vendor_orphan_removed ("vendored dir had no ledger entry").
  • No vendor_takeover_reverted_redirect event, and --dry-run shows no vendor_would_revert_redirect either.

The vendor command on the same project works: it uses the discovered lowercase purl, so the takeover runs (eject_planned), the ledger is written, the config gets the local feed and list says vendored. So this is specific to the scan --mode vendored path (and likely get --mode vendored, which shares it).

Same root cause, smaller symptom: on a hosted project, remove pkg:nuget/Newtonsoft.Json@13.0.3 and rollback pkg:nuget/Newtonsoft.Json@13.0.3 (the purl spelling scan itself prints) fail not_found (exit 1). remove pkg:nuget/newtonsoft.json@13.0.3 works (hosted_reverted). NuGet ids are case-insensitive, so either spelling should match.

Expected (CLI_CONTRACT.md, "Takeover reconciliation (every hosted ecosystem, v5.0)")

vendoring over a hosted pin (vendor, scan --mode vendored, get --mode vendored) first RESTORES that purl's lock entries to their default upstream registry entry … and then vendors, so the vendor ledger records the PRISTINE registry entry … The run that takes over records a vendor_takeover_reverted_redirect advisory event

--dry-run should report vendor_would_revert_redirect.

Actual

== scan --mode vendored --vendor-source service   (over a hosted project)
 rc=0  status success
 ev applied pkg:nuget/Newtonsoft.Json@13.0.3
 ev skipped vendor_prebuilt_downloaded …
 ev skipped vendor_artifact_rebuilt the committed vendored nupkg for Newtonsoft.Json@13.0.3 was missing or stale; rebuilt at .socket/vendor/nuget/<uuid>/newtonsoft.json.13.0.3.nupkg (nuget.config untouched)
nuget.config: <add key="socket-patch-<uuid>" value="https://patch.socket.dev/patch-registry/nuget/<token>/<uuid>/index.json" />
.socket/vendor/: nuget/ only (no state.json)
list: [('pkg:nuget/newtonsoft.json@13.0.3', 'hosted')]

== vendor --vendor-source service   (control, same starting state)
 rc=0  status success
 ev applied pkg:nuget/newtonsoft.json@13.0.3
.socket/vendor/: nuget/ state.json
list: [('pkg:nuget/newtonsoft.json@13.0.3', 'vendored')]

== remove pkg:nuget/Newtonsoft.Json@13.0.3   (hosted project)
 rc=1  not_found: No patch found matching identifier: pkg:nuget/Newtonsoft.Json@13.0.3
== remove pkg:nuget/newtonsoft.json@13.0.3
 rc=0  removed hosted_reverted

Repro

This uses the wiremock Backend stand-in and real nuget.org fixture restore from crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs (API + hosted flat-container feed), with a real dotnet restore:

# fixture: app.csproj (net8.0, RestorePackagesWithLockFile, Newtonsoft.Json 13.0.3) + nuget.org-only nuget.config,
# restored once so packages.lock.json exists
sp() { socket-patch "$@" --json --yes --api-url $URI --org test-org --api-token fake --patch-server-url $URI; }
sp scan --mode hosted                                   # wires socket-patch-<uuid> → hosted feed, re-pins lock
# (optional) sed the source URL to https://patch.socket.dev/... — same result
sp scan --mode vendored --vendor-source service         # exit 0, success, no takeover
grep socket-patch nuget.config                          # still the hosted URL
ls .socket/vendor                                       # nuget/ only, no state.json
sp list                                                 # mode: hosted
NUGET_PACKAGES=$(mktemp -d) dotnet restore --locked-mode   # NU1803 (http stand-in): downloads from the hosted feed
sp vendor --revert                                      # vendor_orphan_removed, deletes the nupkg

Matrix

OS SDK config URL autocrlf scan --mode vendored over hosted vendor over hosted remove <Mixed.Case purl> on hosted
Linux 8.0.131 http stand-in false fail (4 runs) pass fail (not_found)
Linux 8.0.131 http stand-in true fail untested untested
Linux 8.0.131 https://patch.socket.dev/… (sed) false fail untested untested

The bug is in OS-independent purl matching, so macOS and Windows weren't probed.

First bad: current main 61cfb9b (the v5 lockfile-discovered takeover from #280). v4.0.0 used the redirect ledger, so I didn't bisect further.

Suspect code

  • crates/socket-patch-cli/src/commands/vendor.rs:2168-2172: hosted_pin_of compares canonical_purl(&pin.purl) == canonical_purl(purl).
  • crates/socket-patch-core/src/utils/purl.rs:152: canonical_purl only strips qualifiers and percent-decodes. It doesn't fold case for case-insensitive ecosystems (NuGet; the composer case-folding in purl_eq shows the intended pattern).
  • crates/socket-patch-core/src/vendor/nuget_feed.rs:257: config_wired = config_text.contains(&source_key) can't tell a hosted socket-patch-<uuid> source from a vendored one (it doesn't check the value is .socket/vendor/nuget/<uuid>). That turns the missed takeover into a silent false success instead of a refusal.
  • crates/socket-patch-cli/src/commands/remove.rs:40 (hosted_pins_matching) and rollback's purl targeting: the same case-sensitive comparison.

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