[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
- 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).
- Hosted
rewrite_nuget and upstream::nuget::restore use it, so they pin only the entries at the patched version. Delete both hand-rolled walks.
- 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
Dependencies
Independent of #561 and E11, although all three touch rewrite_nuget; land them in one sequence to avoid conflicts.
[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: E50.
Kind: bug. Source: new finding; register E50.
Problem
packages.lock.jsonis walked by four code paths, and hosted and vendored disagree about which entries belong to the patched package.locked_atonly takes entries whoseresolvednormalizes to the patched version. Its own test says so: "A different resolved version is not our package → nothing to pin".``rewrite_nugetrewrites every entry of the id, in every target framework, whatever itsresolvedis. It setsresolvedto the patched version andcontentHashto the patched hash:upstream::nuget::restore`` is version-blind too: it sets every entry of the id to the restored version.nuget_lock_entries(the vendored walker without the version filter).Repro (unit test, run twice on main
203e092). A multi-targeting lock wherenet6.0resolvesNewtonsoft.Json12.0.3 andnet8.0resolves 13.0.3, with a hosted patch for 13.0.3:rewrite_registry_redirect(hosted):net6.0becomes{"requested":"[12.0.3, )","resolved":"13.0.3","contentHash":"PATCHED=="}, with 2redirect_nuget_lockedits.edit_lock(vendored), same lock and patch:net6.0stays{"resolved":"12.0.3","contentHash":"OLD12=="}.So hosted silently upgrades the
net6.0dependency 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-frameworkPackageReferenceversions, 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
NugetLockEntry/nuget_lock_entries/locked_atandnormalize_nuget_versionfromvendor/nuget_feed.rsintoformats::nuget(a lock section next to the config reader), with a mutable variant that yields(framework, id, &mut entry).rewrite_nugetandupstream::nuget::restoreuse it, so they pin only the entries at the patched version. Delete both hand-rolled walks.nuget_lock_other_version, documented inCLI_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
net6.0untouched and refuses with the shared code, and so does vendored.packages.lock.jsonwalker; noget("dependencies")walk of a NuGet lock is left inredirect/.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.