fix(dotnet): resolve packages.lock dependencies deterministically - #5210
fix(dotnet): resolve packages.lock dependencies deterministically #5210spiffcs wants to merge 2 commits into
Conversation
…ramework Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
… target frameworks Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
ac8570f to
c60cb64
Compare
CAOShurong
left a comment
There was a problem hiding this comment.
End-to-end verification on the current head (c60cb64), Windows, syft built from the exact head with the issue's multi-target scenario (Newtonsoft.Json Direct under net8.0 / Transitive under netstandard2.0, Serilog pulled transitively via MyLogAdapter):
- determinism, 100 scans of the same fixture: main flips (88 Direct / 12 Transitive), this head is 100/100 "Direct" — the reported nondeterminism is gone and Direct-wins is stable;
- relationships resolve per-framework correctly and dedupe across frameworks: the fixture yields exactly one "Serilog dependency-of MyLogAdapter" edge, and Serilog itself is reported Transitive as every framework sees it;
- package order and relationship order are stable across runs.
One scope note (matches what the PR already implies): metadata.requested from the lockfile is still not surfaced (metadata exposes name/version/type/contentHash only), so the second observation in #5211 remains open beyond this PR — worth a tracking note so it isn't lost when this merges.
|
Independent end-to-end verification of #5210 (maintainer fix for #5211), run 2026-08-24 ~02:20+08:00. Setup
Baseline (main): nondeterminism confirmed on Windows too 60 scans of the byte-identical input:
This reproduces the issue's flip-rate (~17% here vs ~16% in the issue) outside Linux, so the randomized-map-order root cause is platform-independent. Also confirmed on main: Candidate (#5210 head c60cb64): deterministic Same loop, same machine: 60/60 runs → Tests
One observation, not a blocker The fix resolves the determinism and Direct-preference halves of the issue cleanly. The lossiness half remains open by design: after stabilization the SBOM reports only Nice-to-have if you want belt-and-suspenders coverage: a test asserting that two parses of the multi-framework fixture produce identical package metadata ordering, which would catch any future regression into map-order dependence even before golden-file comparison. Verified as an independent third party; no code changes proposed — the PR looks correct and ready from this side. |
Description
Follow-up to #5143. See #5211 for more details.
While reviewing that fix I saw that the
packages.lock.jsonparser resolves dependency edges against a global map of every package in the lockfile.This resolution would ignore which target framework the edge was declared in.
Multi-targeted projects routinely resolve the same package to different versions per framework. Because the old code looked up the declared version globally and, on a miss, fell back to "any package with this name" via non-deterministic map iteration, edges could point at the wrong version or flip between runs.
Given a lockfile that pins
log4netto2.0.5undernet8.0and1.2.15undernetstandard2.0:Before
Dependency edges were resolved from the global package map. The
netstandard2.0edge (log4net 1.2.15) matched exactly, but a package could fall through tofindPkgByName, which returned whichever name match Go's map iteration happened to yield first.After
Each edge resolves within its declaring framework, so
net8.0→log4net 2.0.5andnetstandard2.0→log4net 1.2.15happens deterministically.Changes:
Type of change
Checklist
Issue references
Found during review of #5143