Repository navigation
Hosted and vendored NuGet reject a packages.lock.json with a UTF-8 BOM that dotnet restores fine: hosted skips the redirect and exits 0 success, vendored fails apply_failed #623
Description
[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
If packages.lock.json starts with a UTF-8 BOM (EF BB BF), dotnet restore and dotnet restore --locked-mode read it without complaint. socket-patch's two NuGet lock rewriters parse it with a bare serde_json::from_str, which rejects the BOM:
- Hosted (
scan --mode hosted): the whole NuGet redirect is skipped withredirect_nuget_lock_unparseable("packages.lock.json is not valid JSON"). The run still exits 0 withstatus: successandredirected: 0. Nothing is wired and the project stays unpatched. - Vendored (
scan --mode vendored):apply_failed"unparseable packages.lock.json: expected value at line 1 column 1", exit 1partial_failure. This one is loud, but it refuses a lock the toolchain accepts.
The rest of the codebase already treats a BOM as encoding, not content:
- VEX discovery reads the same lock through
parse_json→vendor::common::parse_json_manifest(vex/discover/mod.rs:1391), which strips the BOM. Its comment says "the rewriters keep one, so a wired BOM lock must stay discoverable". utils::serde::strip_bom, the manifest reader and the npm crawler all strip it.- The npm twin of this bug ("BOM locks are refused") was npm lock rewrites drop CRLF (and hosted drops tab indent), so rollback and vendor --revert are not byte-exact; BOM locks are refused #324, now fixed and closed.
Realism: NuGet writes the lock without a BOM, but a lock that has been saved by a Windows editor or rewritten by Windows PowerShell 5.1 (Set-Content -Encoding UTF8 / Out-File) carries one, and dotnet keeps restoring it, so the user never notices.
Impact
- Hosted: a scan that patched nothing reports
successwith exit 0. The only signal is a warning that calls a valid lock "not valid JSON". CI pipelines that gate on the exit code ship the unpatched package. - Vendored: patching is impossible until the user finds and strips an invisible byte.
Repro (Linux, .NET SDK 8.0.131, main 045d7ec)
This uses the Backend stand-in from crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs (patch API plus the hosted NuGet v3 feed for a patched Newtonsoft.Json 13.0.3) and the same fixture: app.csproj with RestorePackagesWithLockFile, a nuget.org-only nuget.config, and a real dotnet restore.
# fixture restored from nuget.org -> packages.lock.json (no BOM), then:
printf '\xef\xbb\xbf' > l; cat packages.lock.json >> l; mv l packages.lock.json
dotnet restore --locked-mode # rc 0: dotnet accepts the BOM lock
socket-patch scan --mode hosted --json --yes --api-url $URI --org test-org \
--api-token fake-token --patch-server-url $URI
# rc 0, status "success", redirect: {"redirected": 0, "rewrittenFiles": [],
# "warnings": [{"code": "redirect_nuget_lock_unparseable",
# "detail": "packages.lock.json is not valid JSON; nuget redirect skipped"}]}
# nuget.config untouched, lock contentHash still upstream; a fresh-checkout
# `dotnet restore --locked-mode` installs PRISTINE LICENSE.md.
socket-patch scan --mode vendored --vendor-source service --json --yes ... # same flags
# rc 1, status "partial_failure":
# {"action":"failed","errorCode":"apply_failed",
# "error":"unparseable packages.lock.json: expected value at line 1 column 1"}Control: the same project without the BOM passes in both modes (lock re-pinned to the patched contentHash, and the fresh-checkout locked restore installs the PATCHED bytes).
Expected vs actual
- Expected: docs/ecosystems.md (NuGet row) says hosted does "
nuget.configsource + source-mapping,packages.lock.jsoncontentHash rewrite" and vendored does a "packages.lock.jsoncontentHash pin". A lock the toolchain reads should be rewritten, with the BOM preserved so a revert stays byte-exact, as for npm after npm lock rewrites drop CRLF (and hosted drops tab indent), so rollback and vendor --revert are not byte-exact; BOM locks are refused #324. At minimum, a hosted run that redirects nothing because of a refusal shouldn't reportsuccess/ exit 0. - Actual: hosted skips silently with exit 0, and vendored refuses.
Matrix
| OS | SDK | mode | result |
|---|---|---|---|
| Linux | 8.0.131 | hosted | fail (exit 0, nothing wired, restore unpatched); 3/3 runs |
| Linux | 8.0.131 | vendored | fail (apply_failed, exit 1); 3/3 runs |
| Linux | 8.0.131 | hosted / vendored, no BOM | pass |
| macOS / Windows | — | — | untested. The defect is OS-independent parsing; the BOM is just more common on Windows. |
First bad version
This isn't a regression. On release v4.0.0 (npm), hosted fails in a different way: it wires nuget.config (redirected: 1, rewrittenFiles: ["nuget.config"]) but can't parse the lock, so the upstream contentHash stays, and the fresh-checkout locked restore fails NU1403: Package content hash validation failed for Newtonsoft.Json.13.0.3. Vendored on v4.0.0 is the same apply_failed.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:5015:serde_json::from_str::<Value>(text)on the raw lock text. Compare line 3460 in the same file, which already doest.strip_prefix('\u{feff}').crates/socket-patch-core/src/vendor/nuget_feed.rs:1261-1262:lock_value→serde_json::from_str, used byedit_lock(1279) andlock_pinned(1318).- The helpers already exist:
vendor::common::parse_json_text/parse_json_manifest. ThecontentHashstring surgery keeps all the other bytes, so the BOM would survive.
No probe branch was run: the parse is platform-independent.
Activity
mikolalysenko commentedon Oct 3, 2026
[agent] Triaged as
priority:p3(NuGet). Confirmed onmainthat the hosted rewriter still emitsredirect_nuget_lock_unparseablefor this lock (patch/redirect/mod.rs) and doesn't run it through the BOM-strippingparse_json_manifestthat VEX discovery uses. No open PR covers it yet.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Structural cause tracked in #905. There is no shared BOM helper: four named
strip_bomcopies plus about 50 inline strips. The NuGet lock rewriters are among the readers that skipped it. Once #905 lands, this fix should call itsformats::text::strip_bom/split_bomrather than add another copy.Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] The hosted writer half (LF, no BOM, whole-file rewrites of a CRLF
packages.lock.json) is tracked in #1068.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 release blocker (P1). A normal dotnet-readable UTF-8 BOM lockfile must be accepted consistently by hosted and vendored rewrites.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[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-048351
Pin only the patched NuGet version; read BOM locks