[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
In vendored mode, the nuget.config writer (build_config_edit) and its key reader (parse_config_source_keys) look for section ends with literal find("</packageSources>") / contains("</packageSourceMapping>"). XML allows whitespace before the > of an end tag, and NuGet parses </packageSources > and </packageSourceMapping > fine. When the user's config uses either spelling, vendored decides the section doesn't exist and appends a second <packageSources> or <packageSourceMapping> element before </configuration>. NuGet reads only the first one, so:
</packageSources >: the Socket source sits in the ignored second section. The mapping routes Newtonsoft.Json exclusively to a source that doesn't exist, so every restore fails NU1100.
</packageSourceMapping >: the Socket mapping sits in the ignored second section. The id resolves from nuget.org, so with the re-pinned lock every restore (locked or plain) fails NU1403. Without a lock the unpatched package would install silently.
In both cases scan --mode vendored exits 0 with status: success and no warning, and the in-run --vex attests not_affected / inline_mitigations_already_exist.
Hosted mode already tolerates both spellings (patch/redirect/mod.rs builds </{section}\s*> and </configuration\s*> regexes, and the open tag <packageSources >), and its restores pass on the same fixtures. Only the vendored twin is affected.
Impact
A config that dotnet accepts is turned into one that can't restore, and every signal socket-patch gives says it worked: exit 0, success, and a VEX not_affected for a patch that never installs. The spelling is uncommon (hand-edited or tool-generated configs), so the reach is narrow, but the failure is silent at scan time.
Repro
This uses the wiremock stand-in from crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs: fixture restore of Newtonsoft.Json 13.0.3, then scan --mode vendored --vendor-source service. Only nuget.config differs from the suite's REGISTRY_CONFIG:
<!-- A -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources >
</configuration>
<!-- B -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping >
</configuration>
dotnet restore --locked-mode # baseline: Restored (both A and B)
socket-patch scan --mode vendored --vendor-source service --json --yes \
--api-url $URI --org test-org --api-token fake --vex vex.json --vex-product pkg:nuget/app@1.0.0
# exit 0, status success, no warnings; vex.json: not_affected / inline_mitigations_already_exist
rm -rf obj; NUGET_PACKAGES=$(mktemp -d) dotnet restore --locked-mode
# A: error NU1100: Unable to resolve 'Newtonsoft.Json (>= 13.0.3)' for 'net8.0'. PackageSourceMapping is enabled, the following source(s) were not considered: nuget.org.
# B: error NU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3.
Resulting config for A (the Socket source lands in a second, ignored section):
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources >
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="socket-patch-<uuid>" value=".socket/vendor/nuget/<uuid>" />
</packageSources>
<packageSourceMapping> … nuget.org → * ; socket-patch-<uuid> → Newtonsoft.Json … </packageSourceMapping>
For B, a second <packageSourceMapping> is appended after the user's </packageSourceMapping >.
vendor --revert restores both configs byte-for-byte, so the unwind is fine.
Expected vs actual
- Expected: per CLI_CONTRACT.md (vendored table, nuget row), vendored adds the
nuget.config source plus the packageSourceMapping for the id, verified by dotnet restore --locked-mode on a cold cache. The Socket entries should land in the user's existing sections, as hosted mode does for the same files. If the config can't be edited, it should be refused rather than reported as success.
- Actual: a duplicate section NuGet ignores, exit 0
success, VEX attests, and every restore fails (NU1100 / NU1403).
Matrix
| OS |
SDK |
binary |
A </packageSources > |
B </packageSourceMapping > |
hosted (A and B) |
| Linux |
8.0.131 |
main 045d7ec |
fail NU1100 (2/2) |
fail NU1403 (2/2) |
pass |
| Linux |
8.0.131 |
v4.0.0 (npm) |
fail NU1100 |
fail NU1403 |
n/a |
Not a regression; v4.0.0 behaves the same. macOS and Windows weren't probed (the writer is pure string handling, with no OS dependence).
Suspect code
crates/socket-patch-core/src/vendor/nuget_feed.rs:936: creating_mapping = !visible.contains("</packageSourceMapping>")
crates/socket-patch-core/src/vendor/nuget_feed.rs:974: visible.find("</packageSources>") falls through to the "create the section" branch
crates/socket-patch-core/src/vendor/nuget_feed.rs:987: same literal for the mapping anchor
crates/socket-patch-core/src/vendor/nuget_feed.rs:1069: parse_config_source_keys returns no keys, so nuget.org gets re-seeded as a duplicate <add>
The tokenizer refactor proposed in #594 would subsume this. This issue records the user-visible symptom, which #594 doesn't list.
No probe branch was used. Stale bughunt/nuget/* branches can't be deleted from the sandbox, so new probe branches are on hold; see ledger #320.
[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
In vendored mode, the
nuget.configwriter (build_config_edit) and its key reader (parse_config_source_keys) look for section ends with literalfind("</packageSources>")/contains("</packageSourceMapping>"). XML allows whitespace before the>of an end tag, and NuGet parses</packageSources >and</packageSourceMapping >fine. When the user's config uses either spelling, vendored decides the section doesn't exist and appends a second<packageSources>or<packageSourceMapping>element before</configuration>. NuGet reads only the first one, so:</packageSources >: the Socket source sits in the ignored second section. The mapping routesNewtonsoft.Jsonexclusively to a source that doesn't exist, so every restore fails NU1100.</packageSourceMapping >: the Socket mapping sits in the ignored second section. The id resolves from nuget.org, so with the re-pinned lock every restore (locked or plain) fails NU1403. Without a lock the unpatched package would install silently.In both cases
scan --mode vendoredexits 0 withstatus: successand no warning, and the in-run--vexattestsnot_affected/inline_mitigations_already_exist.Hosted mode already tolerates both spellings (
patch/redirect/mod.rsbuilds</{section}\s*>and</configuration\s*>regexes, and the open tag<packageSources >), and its restores pass on the same fixtures. Only the vendored twin is affected.Impact
A config that dotnet accepts is turned into one that can't restore, and every signal socket-patch gives says it worked: exit 0,
success, and a VEXnot_affectedfor a patch that never installs. The spelling is uncommon (hand-edited or tool-generated configs), so the reach is narrow, but the failure is silent at scan time.Repro
This uses the wiremock stand-in from
crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs: fixture restore ofNewtonsoft.Json13.0.3, thenscan --mode vendored --vendor-source service. Onlynuget.configdiffers from the suite'sREGISTRY_CONFIG:Resulting config for A (the Socket source lands in a second, ignored section):
For B, a second
<packageSourceMapping>is appended after the user's</packageSourceMapping >.vendor --revertrestores both configs byte-for-byte, so the unwind is fine.Expected vs actual
nuget.configsource plus thepackageSourceMappingfor the id, verified bydotnet restore --locked-modeon a cold cache. The Socket entries should land in the user's existing sections, as hosted mode does for the same files. If the config can't be edited, it should be refused rather than reported assuccess.success, VEX attests, and every restore fails (NU1100 / NU1403).Matrix
</packageSources ></packageSourceMapping >045d7ecNot a regression; v4.0.0 behaves the same. macOS and Windows weren't probed (the writer is pure string handling, with no OS dependence).
Suspect code
crates/socket-patch-core/src/vendor/nuget_feed.rs:936:creating_mapping = !visible.contains("</packageSourceMapping>")crates/socket-patch-core/src/vendor/nuget_feed.rs:974:visible.find("</packageSources>")falls through to the "create the section" branchcrates/socket-patch-core/src/vendor/nuget_feed.rs:987: same literal for the mapping anchorcrates/socket-patch-core/src/vendor/nuget_feed.rs:1069:parse_config_source_keysreturns no keys, so nuget.org gets re-seeded as a duplicate<add>The tokenizer refactor proposed in #594 would subsume this. This issue records the user-visible symptom, which #594 doesn't list.
No probe branch was used. Stale
bughunt/nuget/*branches can't be deleted from the sandbox, so new probe branches are on hold; see ledger #320.