Skip to content

Hosted NuGet rewrites packages.lock.json entries at other versions of the patched id #593

Description

[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: E50.

Kind: bug. Source: new finding; register E50.

Problem

packages.lock.json is walked by four code paths, and hosted and vendored disagree about which entries belong to the patched package.

  • Vendored locked_at only takes entries whose resolved normalizes to the patched version. Its own test says so: "A different resolved version is not our package → nothing to pin".``
  • Hosted rewrite_nuget rewrites every entry of the id, in every target framework, whatever its resolved is. It sets resolved to the patched version and contentHash to the patched hash:
    for (id, entry) in fw.iter_mut() {
        if id.to_lowercase() == id_lower {
            // … no check of entry["resolved"] against dep.version …
            obj.insert("resolved".into(), Value::String(resolved.clone()));
            obj.insert("contentHash".into(), Value::String(content_hash.clone()));
  • Hosted restore upstream::nuget::restore`` is version-blind too: it sets every entry of the id to the restored version.
  • VEX uses nuget_lock_entries (the vendored walker without the version filter).

Repro (unit test, run twice on main 203e092). A multi-targeting lock where net6.0 resolves Newtonsoft.Json 12.0.3 and net8.0 resolves 13.0.3, with a hosted patch for 13.0.3:

"net6.0": { "Newtonsoft.Json": { "type": "Direct", "requested": "[12.0.3, )", "resolved": "12.0.3", "contentHash": "OLD12==" } },
"net8.0": { "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.3, )", "resolved": "13.0.3", "contentHash": "OLD13==" } }
  • rewrite_registry_redirect (hosted): net6.0 becomes {"requested":"[12.0.3, )","resolved":"13.0.3","contentHash":"PATCHED=="}, with 2 redirect_nuget_lock edits.
  • edit_lock (vendored), same lock and patch: net6.0 stays {"resolved":"12.0.3","contentHash":"OLD12=="}.

So hosted silently upgrades the net6.0 dependency to the patched version's bytes in the lock. Restore can't undo it, because it rebuilds every entry at the restored version and the 12.0.3 pin is gone.

Symptoms

None filed yet. Related lock-selection bugs in the same rewriters: #514 (per-project packages.<project>.lock.json) and #353 (member-project locks).

Impact

Multi-targeting projects (<TargetFrameworks> with per-framework PackageReference versions, or a transitive pin that differs per framework) get a lock that doesn't match their project files. In locked mode NuGet then refuses restore (NU1004), and in unlocked mode it silently re-resolves. The vendored path avoids the lock damage, but it routes the id to a feed that only serves the patched version, so that framework can't restore either. Neither mode tells the user. Size: the four walkers are about 120 production lines.

Proposed change

  1. Move NugetLockEntry / nuget_lock_entries / locked_at and normalize_nuget_version from vendor/nuget_feed.rs into formats::nuget (a lock section next to the config reader), with a mutable variant that yields (framework, id, &mut entry).
  2. Hosted rewrite_nuget and upstream::nuget::restore use it, so they pin only the entries at the patched version. Delete both hand-rolled walks.
  3. Shared gate in the same module: if the lock resolves the patched id at another version in some framework, refuse in both hosted and vendored modes with one new code (for example nuget_lock_other_version, documented in CLI_CONTRACT.md). The id-level <packageSourceMapping> would route that framework to a feed that doesn't serve its version.

Size and scope

formats/nuget/, patch/redirect/mod.rs (the NuGet lock block), patch/redirect/upstream/nuget.rs, vendor/nuget_feed.rs, vex/discover/nuget.rs (import path only). About +80 / −90 production lines. Out of scope: the config reader and splice anchors (E11) and multi-version support itself.

Acceptance criteria

  • The repro above is a regression test: hosted leaves net6.0 untouched and refuses with the shared code, and so does vendored.
  • One packages.lock.json walker; no get("dependencies") walk of a NuGet lock is left in redirect/.
  • Restore of a hosted pin leaves other-version entries untouched.
  • Existing NuGet hosted, vendored, restore and VEX tests stay green (cargo test -p socket-patch-core --lib nuget, e2e_nuget*).

Dependencies

Independent of #561 and E11, although all three touch rewrite_nuget; land them in one sequence to avoid conflicts.

Activity

  1. added
    bugSomething isn't working
    arch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)
    on Oct 2, 2026
  2. added a commit that references this issue on Oct 2, 2026
  3. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p3 (NuGet). Distinct from #561/#585/#594 (those are nuget.config reading; this is the packages.lock.json walk). Note that open PR #597 also edits rewrite_nuget, so this should land after it. No open PR covers it.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Real-dotnet confirmation from the scheduled NuGet / dotnet bug-hunt routine (ledger #320). Main 045d7ec, Linux, SDK 8.0.131. Reproduced twice.

    Fixture: <TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>, RestorePackagesWithLockFile, with Newtonsoft.Json 13.0.3 for net8.0 and 12.0.3 for netstandard2.0. Patch for 13.0.3. Wiremock stand-in from e2e_nuget_dotnet_build.rs.

    One correction to the impact section: in hosted mode, locked mode does not refuse.

    mode scan lock diff fresh dotnet restore --locked-mode fresh plain dotnet restore
    hosted exit 0, success, redirected: 1, no warning netstandard2.0: resolved 12.0.3 → 13.0.3, contentHash → patched hash (requested stays [12.0.3, )) succeeds. netstandard2.0 silently gets 13.0.3, because [12.0.3, ) admits it succeeds, only 13.0.3 in the store
    vendored exit 0, success, no warning only the net8.0 entry is re-pinned NU1102 Unable to find package Newtonsoft.Json with version (>= 12.0.3) … Found 1 version(s) in socket-patch-<uuid> … Versions from nuget.org were not considered NU1102 (same)

    What this means in practice:

    • Hosted silently upgrades the other framework's dependency across a major version, and nothing reports it.
    • Vendored breaks every restore of the project while still reporting success.

    Both outcomes support the shared refusal (nuget_lock_other_version) that the issue proposes.


    Generated by Claude Code

  5. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-checked on main 05ecc6e. The code moved, and the defect is unchanged.

    • Hosted rewrite_nuget still matches entries by id alone (id.to_lowercase() == id_lower), in every framework, and overwrites resolved and contentHash with no check of the entry's own resolved against the patched version.
    • Vendored locked_at still filters on normalize_nuget_version(e.resolved) == version_norm.
    • Hosted restore is in upstream/nuget.rs, with the same version-blind walk.

    No open PR claims this issue.


    Generated by Claude Code

  6. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-checked on main @ f3c6313 (architecture audit, ecosystems and formats). This still holds, but the code moved. Hosted rewrite_nuget is now at redirect/mod.rs#L5599. Its lock walk at #L5716-L5760 still matches on id only and overwrites resolved/contentHash in every framework, without comparing the entry's resolved to the patched version.


    Generated by Claude Code

  7. added
    v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.
    compatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.
    and removed on Oct 9, 2026
  8. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 release blocker (P1). A hosted NuGet rewrite must match both package id and version. Rewriting other versions corrupts an otherwise ordinary multi-target lockfile.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  9. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming for v5 blocker burn-down (shared root cause: hosted rewrite_nuget walks packages.lock.json with its own bare serde_json + id-only matcher instead of the shared BOM-tolerant, version-filtered lock reader). Branch: agent/v5-nuget-lock-reader. Claim-ID: 2026-10-09T16:44Z-47bc41

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:claimedagent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingcompatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.pm:nugetNuGet / dotnetpriority:p1v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions