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

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:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpm:nugetNuGet / dotnetpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions