Skip to content

fix(sc4): keep npm link slots from borrowing ancestor pins - #1106

Merged
rng1995 merged 1 commit into
mainfrom
naren/npm-link-slot-boundary
Nov 13, 2026
Merged

rng1995 merged 1 commit into
mainfrom
naren/npm-link-slot-boundary

Conversation

@rng1995

@rng1995 rng1995 commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #940 for the open review thread on link: true slots (comment).

npm lockfileVersion 2/3 records a local-link install as {"resolved": ..., "link": true}, with no name or version. _collect_npm_locked_versions stored nothing for such a slot, so it was missing from its owner's layer and ChainMap returned an ancestor's registry pin instead. Example: the root installs lodash@4.17.20 and packages/a/node_modules/lodash links to a local package. The workspace's lodash: file:... dependency then resolved to 4.17.20, and SC4 reported that release's advisories against packages/a/package.json as a version-matched finding.

Changes

  • _collect_npm_locked_versions: every occupied node_modules install slot now writes its name into its owner's layer, because Node's module lookup finds the nearest slot first. A slot keeps its own version unless it is an alias, a link: true install or has no version. Those stay unresolved (None), which is how alias slots were already handled. The alias branch is folded into the same rule without a behavior change.
  • Tests:
    • test_npm_workspace_link_shadows_ancestor_registry_pin covers three slot shapes (a link, a link with a stray version, a versionless slot) across lockfileVersion 2 and 3, package-lock.json and npm-shrinkwrap.json, plain and scoped names, and component order.
    • test_npm_workspace_link_does_not_send_the_ancestor_pin_to_osv checks end to end, through node(), that OSV is never asked about the root's version for the workspace.
  • CHANGELOG: one line under the fix(sc4): scope npm lock resolution to owning projects #940 entry.

Behavior

  • A workspace manifest dependency installed as a link now resolves to ('lodash', None) instead of ('lodash', '4.17.20'). OSV is queried by name only, which yields at most a LOW "Unverifiable Dependency" result, the same as for an alias slot. This also closes the opposite case: a patched root pin no longer hides a vulnerable vendored copy behind the link.
  • Unchanged:
    • The ancestor install is still audited through its lockfile.
    • Registry, git, tarball and file: installs that record a version keep it.
    • Sibling workspaces and the root still resolve to the root pin.
    • lockfileVersion 1 already records a link's file: spec in its own slot, so it never borrowed an ancestor pin.

Validation

  • All 49 new test cases fail on main and pass with this change.
  • ruff check and ruff format --check pass on the changed files.
  • A differential probe over 1,512 lockfile entry shapes gave the same result as main everywhere except unpinned, non-alias slots, which now stay unresolved.
  • Full unit suite (-m "not integration and not provider", Python 3.14, run locally on a heavily loaded machine): 25,190 passed, 19 failed. None of the failures involves this change. 18 are wall-clock or timeout tests (subprocess cold imports, worker startup, elapsed-time limits) that fail the same way on main (17c4d38) or pass and fail at random under load. test_deep_npm_own_lock_keeps_parse_error_without_borrowing_parent_pin fails identically on main, because on Python 3.14 json no longer raises RecursionError for deeply nested input (CI uses Python 3.12).
  • Hosted CI on fea2ca4: all checks passed, including test-unit; publish was skipped.

🤖 Generated with Claude Code

A lockfileVersion 2/3 `link: true` install records `resolved` and `link`
but no `name` or `version`, so its slot was left out of the nearest
layer and ChainMap returned an ancestor's registry pin for it. A
workspace whose `lodash: file:...` dependency is linked to local code
then inherited the root's `lodash@4.17.20`, and SC4 attributed that
release's advisories to the workspace.

Record every occupied install slot in its owner's layer, as Node's
module lookup does. An alias, a `link: true` install or a slot without
a version stays unresolved (None), as alias slots already did; any
other slot keeps its own version. The ancestor install is still
audited through its lockfile.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rng1995
rng1995 merged commit cd7acfb into main Nov 13, 2026
6 checks passed
@rng1995
rng1995 deleted the naren/npm-link-slot-boundary branch November 13, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant