Repository navigation
Fix NuGet mapping exclusivity and inherited sources (#354, #462) - #1341
Mikola Lysenko (mikolalysenko) wants to merge 12 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Vendored NuGet now reads the source keys and finds the <packageSources>, <packageSourceMapping> and <configuration> anchors through formats::nuget::parse_config, the reader that hosted, upstream restore and VEX already use. The private substring scanner (blank_comments, parse_config_source_keys, attr_value, self_closing_package_sources, insert_at_line) is deleted. User impact: - A close tag written with whitespace (</packageSources >) is now the section that gets extended; vendor used to append a second section NuGet ignores, so restore failed NU1100/NU1403 (#685). - An empty <packageSourceMapping /> is expanded in place instead of left beside a second mapping section. - A section opened and closed on one line receives the source inside it, not before its open tag. - Catch-all keys are written XML-encoded, so a key with & or a quote keeps its identity. - Malformed XML or a repeated section is refused with "malformed XML or a repeated section; not wired" instead of being spliced at the first substring match, as hosted already does. Output bytes for well-formed configs are unchanged. Fixes #685 Refs #594 Assisted-by: Claude Code:claude-opus-5-5
…-config' into agent/v5-nuget-mapping
Draft placeholder while the fix is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When vendored or hosted mode created a packageSourceMapping, its `*` catch-all named only the sources of the one nuget.config it edited. NuGet merges the user config and every parent directory's config, and once a mapping exists it drops every source no pattern names, so private feeds defined outside the project failed NU1101 (#354). A fresh config also re-added nuget.org a parent had cleared for a mirror. The catch-all now also names the sources NuGet inherits (user config, then parent directories, honoring <clear />), and nuget.org is only seeded when those configs have it. Hosted gets them from the engine through a synthetic candidate key; the in-memory engine keeps the file-only reading. When another source already mapped the patched id exactly (Visual Studio's mapping UI writes such lists), the two tied and NuGet took the package from whichever feed answered first: NU1403 or silently unpatched (#462). That pattern is now commented out in a marker naming the Socket source while the patch is wired, and vendor --revert, remove and rollback put it back byte-exact. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
BugBot review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e5e3c5a. Configure here.
| } else if let (Some(idx), Some(before)) = (open_mapping, patterns) { | ||
| if cfg.mappings[idx].1.len() > before { | ||
| cfg.mapping_spans[idx].patterns.push(at..i); | ||
| } |
There was a problem hiding this comment.
Set-aside omits package close tags
Medium Severity
Pattern spans record only the open tag of a <package> element. When that tag is not self-closing, set_aside_competing_patterns comments the opener and leaves a dangling </package>, which makes the config unparseable while the patch is wired.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e5e3c5a. Configure here.
There was a problem hiding this comment.
[agent] Fixed in c696181: the pattern span now runs through </package> for a non-self-closing element, so the whole element is set aside and restored byte-exact (test extended with the open/close spelling).
A <package> pattern written with a close tag (<package pattern="x"> </package>) was set aside from its open tag only, leaving a dangling </package> that made nuget.config unparseable while the patch was wired (Bugbot on #1341). The pattern span now runs through the close tag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| } else { | ||
| inherited | ||
| }; | ||
| for key in inherited { |
There was a problem hiding this comment.
[agent] This fans the created * catch-all out to every inherited source key, but nuget_config::inherited_source_keys (nuget_config.rs:94) only collects inherited sources, not inherited <packageSourceMapping>. NuGet merges mappings across the config chain, so if a parent/user NuGet.Config already maps e.g. nuget.org to Newtonsoft.* only and * to corp, the child's new <packageSource key="nuget.org"><package pattern="*" /> widens nuget.org to every package — re-opening the dependency-confusion path the parent mapping closes. Fix: when an inherited mapping exists, don't fan * to keys it already covers (or leave the inherited mapping governing non-socket packages and only add the socket pattern).
There was a problem hiding this comment.
[agent] Fixed in 8e1d8e4. (1) Inherited mappings: inherited_sources now also reports whether any inherited config maps packages; when one does, neither writer writes a * catch-all (only the Socket pattern), since NuGet merges the inherited patterns and a catch-all here would widen a restricted source. Tests: inherited_mapping_gets_no_catch_all, inherited_mapping_is_reported, hosted nuget_created_catch_all_names_inherited_sources. (2) Set-aside: only the key this run wires is exempt; socket-patch-* lookalikes and stale uuids are set aside like any other competitor (test in competing_exact_patterns_are_set_aside_and_restored).
The hosted engine asked the project view for its raw root to read the user and parent-directory NuGet configs, which ends a re-scan read cache's recording. It now takes the root without that, reads the configs beside the view, and hands the view every path it probed so the cache fingerprints them like its own reads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # crates/socket-patch-core/src/formats/nuget/mod.rs # crates/socket-patch-core/src/vendor/nuget_feed.rs
NuGet merges packageSourceMapping across the config chain too. When a parent or user config already maps packages, a `*` catch-all written into the project's config widened a source the parent restricts (review on #1341). The writers now write no catch-all then, only the Socket pattern: the inherited patterns already route everything else. The exclusivity set-aside skipped every socket-patch-* key, so a lookalike key (or a stale uuid) pointing at any feed stayed tied with the Socket source (security review on #1341). Only the source this run wires is exempt now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
|
||
| /// The comment a writer sets a competing mapping element aside in while the | ||
| /// Socket source `key` is wired: `<!-- {key} moved: {element} -->`. | ||
| fn set_aside_open(key: &str) -> String { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
Description: Hosted NuGet set-aside writes the patch-service uuid into an XML comment with no canonical-uuid or -- check. A uuid containing --> closes that comment and the remainder is committed as live nuget.config markup. Attribute encoding on the <add key> / <packageSource key> writes does not cover this prefix.
Impact: A hosted NuGet grant is only supposed to add one source and an exact-id mapping. On a file that already has another source's exact pattern, a non-canonical patch_uuid can rewrite that packageSourceMapping in place: <clear /> plus a <packageSource> whose key is not a real source makes restore skip every configured feed for every package id. NuGet keeps only the first packageSources and first packageSourceMapping element, so a second <packageSources><add> after an existing sources section is ignored and does not become an extra feed. When packageSourceMapping is ordered before packageSources, the same breakout can emit the first packageSources element (clear plus an attacker URL) and empty the mapping, so that URL is the only feed restore uses. That is a larger blast radius than the single package the grant was allowed to redirect. Cargo rewrite and vendored NuGet already reject a non-canonical uuid before writing config.
Remediation: In rewrite_nuget, skip the dependency unless patch_uuid passes is_canonical_uuid, matching rewrite_cargo and vendor_nuget. Also fail set_aside_competing_patterns when the generated key contains --, so the comment prefix cannot close itself.
Resolve conflicts: - redirect/mod.rs: keep the PR's inherited-source/mapping synthetic keys and EMPTY_NUGET_CONFIG alongside main's PACKAGES_LOCK import. - CLI_CONTRACT.md nuget row: keep the PR's exclusive/inherited mapping text and main's lock-at-other-version refusal (vendor_nuget_lock_other_version). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Picks up #1336 (Hatch pylock.toml lock-only rewrite); no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>


LLM Description written by Claude Code:claude-opus-5-5
Fixes #354
Fixes #462
Summary
The
packageSourceMappingthat vendored and hosted NuGet write is now both inherit-safe (#354) and exclusive (#462).Root cause
Both writers computed the mapping from the single
nuget.configthey edit, and assumed "most specific pattern wins":*out only to that file's sources. NuGet merges the user config and every parent directory's config, and once a mapping exists it drops every source no pattern names. Feeds defined outside the file (dotnet nuget add source, a repo-root config above the project) then failed NU1101. A fresh config also re-seedednuget.orgeven when a parent had<clear />ed it for a mirror.Fix
formats::nuget: the reader recordssources_cleared. A<clear />drops the sources the file defined before it.effective_source_keysmerges a chain of configs, farthest first. It also recordsmapping_spans(byte ranges of each<packageSource>and its pattern tags).vendor::nuget_config::inherited_source_keysreads the chain: the user config (DOTNET_CLI_HOMEorHOME/.nuget/NuGet/NuGet.Config,%APPDATA%on Windows; when absent, NuGet's implicitnuget.org), then each parent directory's config from the root down.<clear />s them.nuget.orgis seeded only when the inherited set has it or is empty. In the common case (the user config has only nuget.org) the output is byte-identical to before. The hosted engine passes the inherited keys to the pure rewriter through a synthetic candidate key (as sbt does). The in-memory engine keeps the file-only reading. The upstream restore drops a pure*fan-out that covers the file's sources (it may name inherited ones too).set_aside_competing_patternscomments out every other (non-Socket) source's exact pattern for the id, in<!-- socket-patch-<uuid> moved: … -->. When that pattern was the element's only one, the whole<packageSource>is commented, since NuGet rejects an element with no pattern.restore_set_asideturns the comments back into the original bytes. Hosted warnsredirect_nuget_mapping_set_aside, and vendored warnsvendor_nuget_mapping_set_aside. Markup that can't go in a comment is skipped withredirect_nuget_mapping_conflict(vendored fails the package). The vendored whole-file revert and its excision path, and the hosted upstream restore (remove/rollback), all restore the pattern. VEX already treats only an exclusive mapping as live, so it now sees the wiring as exclusive.Per-issue tests
vendor::nuget_config::tests::inherited_sources_follow_nugets_merge_order(user config, parent config,<clear />, implicit default);vendor::nuget_feed::tests::created_catch_all_names_inherited_sources,fresh_config_follows_the_inherited_sources,vendor_keeps_a_parent_directorys_feed_routable(end to end throughvendor_nuget);patch::redirect::tests::nuget_created_catch_all_names_inherited_sources;formats::nuget::tests::clear_drops_earlier_and_farther_sources.formats::nuget::tests::competing_exact_patterns_are_set_aside_and_restored;patch::redirect::tests::nuget_competing_exact_pattern_is_set_aside(the issue's config, plus an idempotent re-run);vendor::nuget_feed::tests::competing_exact_mapping_is_set_aside_and_restored(revert through the excision path);upstream::nuget::tests::a_set_aside_pattern_is_restored.Red→green: before the fix, the inherited-source tests get a catch-all of the file's keys only, and the #462 tests leave two sources with the exact id.
Commands run
cargo test -p socket-patch-core --no-fail-fast: greencargo test -p socket-patch-cli --all-features --test e2e_nuget(21) and--test e2e_nuget_dotnet_build -- --ignored(2, local SDK 8.0.129): greencargo clippy --workspace --all-features -- -D warnings: clean.cargo fmt --check: clean for touched files.Review follow-ups
packageSourceMappinggets no*catch-all, only the Socket pattern, because NuGet merges inherited mappings and a catch-all would widen a source they restrict.socket-patch-*lookalike key or a stale uuid that names the id is set aside like any other competitor (security review).<package>element written with a close tag is set aside whole (Bugbot).Notes for review
/etc/opt/NuGet/Config,%ProgramFiles(x86)%\NuGet\Config) and~/.nuget/config/*.configaren't read. A source added only at restore time on another machine (e.g. CI'sdotnet nuget add sourceunder a name the scanning machine lacks) still needs a mapping entry.🤖 Generated with Claude Code
Note
Medium Risk
Changes how
nuget.configand hosted redirects are authored—incorrect mapping could break restores or route packages to the wrong feed—but behavior is fail-closed with explicit warnings and broad unit/e2e coverage.Overview
NuGet
packageSourceMappingis now written to match how NuGet actually merges configs and resolves patterns, for both vendored feeds and hosted redirects.When creating a catch-all
*mapping, the writers now include package sources inherited from the user config and parent-directoryNuGet.Configfiles (honoring<clear />), and only seednuget.orgwhen that inherited set still has it—so private feeds above the project stay routable and mirrors that cleared nuget.org are not reintroduced. Hosted scan loads those keys from disk via a synthetic candidate; vendoring uses the sameinherited_source_keyshelper.When another feed already maps the patched package id exactly (a common Visual Studio pattern), that competing pattern is temporarily commented out in
<!-- socket-patch-<uuid> moved: … -->while Socket wiring is active, with restore onrollback/remove/vendor revert. Un-commentable markup skips the redirect withredirect_nuget_mapping_conflict; successful set-asides warn asredirect_nuget_mapping_set_aside/vendor_nuget_mapping_set_aside.The shared
formats::nugetparser now trackssources_cleared, mapping byte spans, and replaces vendored config editing’s ad-hoc XML scanning with the same tokenizer hosted/restore/VEX use. CLI_CONTRACT.md documents the new redirect codes and vendored NuGet behavior.Reviewed by Cursor Bugbot for commit e5e3c5a. Configure here.