Skip to content

Fix scans missing renamed yarn copies (#939, #1236) - #1489

Merged
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-yarn-renamed-copy-identity
Oct 13, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-yarn-renamed-copy-identity

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #939
Refs #1236

Summary

Yarn keys a file: tarball, file: directory, URL tarball and berry portal: / link: dependency by the name the depender gave it. "lp2": "file:./left-pad-1.3.0.tgz" locks as lp2@file:…, but node_modules/lp2 is really left-pad@1.3.0. No pin or vendored wiring of left-pad reaches that copy, so it installs unpatched. Before this PR:

Root cause

Only VEX discovery could work out which package a renamed copy holds (record_copies in vex/discover/yarn.rs), and it skipped portal: / link:. The hosted rewriters and vendored backends key their checks on the lock entry's own name, so they never see the copy, and each used a weaker reader or none.

Fix

  1. One copy reader (formats::yarn::copy::CopyRef): from a classic block or a berry locator it names the copy (Tarball, Directory, Linked for portal:/link:, Url) and reads its package: the package.json inside a tarball, a directory's package.json, the registry package a URL's path names, or for portal:/link: the target's package.json name and version (the lock records 0.0.0-use.local). VEX discovery now uses it and also records portal: / link: copies.
  2. Hosted (hosted/engine.rs): the run already checks its own result with lockfile discovery and names each pnpm copy a pin can't reach (redirect_copy_unpatched, Warn during pnpm scans when non-registry copies cannot be patched #935). That pass now covers yarn.lock too, so every renamed copy discovery sees is named and the in-run VEX never assumes the patch applied. A copy a yarn classic skip warning already names (same lock key) is not named twice.
  3. Vendored (vendor/yarn_copies.rs): lists the renamed copies of name@version in either lock grammar through the same reader. The classic backend now names renamed tarballs too (vendor_yarn_classic_non_registry_entry_skipped). The berry backend names every renamed copy with the new vendor_copy_unpatched and still wires the registry entry.
  4. Docs: docs/ecosystems.md and CLI_CONTRACT.md describe both codes.

Fixing this at the hosted engine's discovery pass covers every yarn copy shape in one place (classic and berry), instead of adding the same name check to each rewriter.

Policy note: portal: / link:

The bug-hunt asked whether a portal:/link: target holding the patched name@version should contest the ref. This PR says yes. Yarn links node_modules/lp2 to that directory and no resolutions pin reaches it, so attesting not_affected beside it is a false attestation. Holding back the attestation is the safe choice. A target holding another package or version contests nothing (tested).

Out of scope (why #1236 stays Refs)

#1236's renamed git copy: yarn 1's lock records no package name for it, so a lock-only reader can't tell it is the patched package without a policy decision. Post-install vex already catches it. #1236 stays open for that.

Also: the in-memory hosted engine (service) only receives the candidate files, not a user's file: tarball or directory, so there the copy can't be read and is not named. Disk scans (the CLI) read it.

Test evidence

New regression tests, each failing on main (verified in a clean origin/main worktree with only the tests applied: 5/5 red) and passing here:

Issue / variant Test
#939 berry portal: / link: lockfile VEX vex::discover::yarn::tests::issue_939_berry_portal_and_link_copies_contest_the_ref
#939 berry hosted: renamed file: tgz / dir / URL / portal: / link: hosted_memory_engine::issue_939_berry_renamed_copy_is_named
#939 berry vendored: same five shapes vendor::yarn_berry_lock::tests::issue_939_other_name_copies_are_named
#1236 classic hosted: renamed file: tarball hosted_memory_engine::issue_1236_classic_renamed_file_tarball_copy_is_named
#1236 classic vendored: renamed file: tarball vendor::yarn_classic_lock::tests::issue_1236_other_name_file_tarball_copy_is_named
shared reader formats::yarn::copy::tests::*

Each test also has a control: a copy holding another package (or, for portal:, another version) is not named and does not contest.

The berry vendored backend also refuses a package installed only under other dependency names as vendor_lock_entry_not_rewritable, naming the copies. It does this in the loop and in the download plan, the same as classic. It used to say "not found, run yarn install", which can't help: vendor::yarn_berry_lock::tests::issue_939_only_other_name_copies_are_refused_as_not_rewritable.

Commands run locally (all on the final head)

  • cargo fmt --all -- --check ✅
  • cargo clippy --workspace --all-features -- -D warnings ✅
  • cargo test -p socket-patch-core --all-features --lib: 6457 passed, 4 failed. The 4 are root-only: copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_… and pypi_requirements::wire_failure_… depend on chmod denying a write, and this sandbox runs as uid 0. None touch yarn code.
  • CLI suites touching yarn: hosted_memory_engine (42), hosted_memory_parity (42), in_process_redirect_pnpm (37), in_process_vendor (133), in_process_rollback_hosted (41), in_process_get_modes, in_process_alternate_installers, covgap_commands_{rollback,scan_hosted,scan_mod,vex}, mode_migration_npm: all pass. The only failures are the same root-only *_write_failure* tests (3 in covgap_commands_vendor, 3 in in_process_redirect). A full cargo test --workspace could not link every test binary within this sandbox's disk allowance, so CI runs the rest.
  • Real-yarn e2e: e2e_redirect_yarn_classic_build (37 ✅), e2e_vendor_yarn_classic_build (38 ✅) on yarn 1.22.22; e2e_redirect_yarn_berry_build (35 ✅), e2e_vendor_yarn_berry_build (42 ✅) on yarn 4.18.0, all with SOCKET_PATCH_YARN_E2E_REQUIRED=1.

/code-review high findings

Fixed: the hosted dedup is scoped to yarn rewriter warnings on the root yarn.lock; the berry only-other-name refusal; the vendored reader skips other-version copies before any read, and skips .socket/ wiring.

Not fixed, and why:

  • The hosted classic rewriter still has its own renamed-copy reader (classic_copy_real_name). Moving it onto CopyRef, or dropping its warning, would rename the documented redirect_yarn_classic_directory_skipped / _non_registry_entry_skipped codes for renamed directories and URLs that Fix yarn classic copies locked under another name (#1236) #1242 shipped and the classic e2e asserts. This PR keeps those codes and lets the engine pass name only what the rewriter can't see (tarballs). Unifying them is a follow-up.
  • A yarn 1 link: copy under another name isn't detected. Yarn 1 VEX and both scans treat every classic link: entry as first-party source, same-name included. Changing that is a policy change beyond these issues, so I left it.
  • ClassicProject keeps the lock text beside its blocks / the tarball is unpacked whole. The tarball read reuses the capped archive decoder VEX already used. With the version pre-filter, vendoring only unpacks copies at the patched version. The extra text is one Arc<str> per run.

🤖 Generated with Claude Code

https://claude.ai/code/session_019yaJ6RS3d3c9ZvVGhYYy9X


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Yarn keys a file: tarball or directory, a url tarball and a berry
portal:/link: dependency by the name the depender gave it, so
"lp2": "file:./left-pad-1.3.0.tgz" locks as lp2@file:... while
node_modules/lp2 is left-pad@1.3.0. Only VEX discovery could tell which
package such a copy really holds, and it skipped berry portal:/link:
copies entirely, so a lock-only vex still attested not_affected while
that copy installed unpatched (#939).

Move the copy-identity reading into formats::yarn::copy (CopyRef), so
every reader takes the package from the copy the same way: the
tarball's or directory's package.json, a registry url's path, or, for
portal:/link:, the target's package.json name and version (the lock
only records 0.0.0-use.local). VEX now records portal:/link: copies too.

Assisted-by: Claude Code:claude-opus-5-5
A hosted scan pinned the registry entry and reported a clean success
while the same yarn.lock also installed the patched name@version under
another dependency name (a renamed file: tarball in yarn classic, any
renamed file:/url/portal:/link: copy in yarn berry). That copy stays
unpatched, and nothing in the scan output said so (#939, #1236).

The run already checks its own result with lockfile discovery and names
each pnpm copy the pin cannot reach (redirect_copy_unpatched). Do the
same for yarn.lock, skipping a copy a yarn classic skip warning already
names, so every renamed copy discovery sees is reported and the in-run
VEX never assumes the patch applied.

Assisted-by: Claude Code:claude-opus-5-5
Vendored scans keyed every check on the lock entry's own name, so a
yarn berry copy of the patched package under another dependency name
got no warning at all, and a yarn classic renamed file: tarball was
missed too (#939, #1236). The resolutions entry or re-lock vendoring
writes never reaches such a copy, so it installs unpatched.

Add vendor::yarn_copies, which lists those copies for either lock
grammar through the shared copy reader. The classic backend now names
renamed tarballs as well, and the berry backend names every renamed
copy with vendor_copy_unpatched while still wiring the registry entry.

Assisted-by: Claude Code:claude-opus-5-5
Describe how hosted and vendored scans and vex treat a yarn copy of the
patched package declared under another dependency name, and list the
widened redirect_copy_unpatched and new vendor_copy_unpatched codes.

Assisted-by: Claude Code:claude-opus-5-5
- hosted: skip a copy only when a yarn rewriter warning names the same
  root yarn.lock entry, so a pnpm or unrelated warning never hides one
- vendored berry: a package installed only under other dependency names
  is refused as vendor_lock_entry_not_rewritable naming those copies
  (in the loop and the download plan), not "not found, run yarn
  install", which can't help
- vendored: never read a copy of another version (no needless tarball
  unpacking) or socket-patch's own .socket/ wiring as a user's copy
- keep the formats layer free of async code: the tarball read off the
  runtime moves to vendor::yarn_copies::copy_identity

Assisted-by: Claude Code:claude-opus-5-5
A berry package installed only under other dependency names is now
refused as vendor_lock_entry_not_rewritable; say so beside the other
renamed-copy behavior.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 13, 2026 18:10
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 36b49c6. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants