[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
When yarn.lock is a symbolic link (a shared lock in a monorepo or Docker build context, e.g. yarn.lock -> ../shared/yarn.lock), scan --mode hosted refuses it with redirect_symlinked_file_unsupported ("…an atomic rename, which would replace the link…; nothing was written", exit 1). scan --mode vendored on the same project has no such gate. It exits 0 with status: "success", and its group commit renames the rewritten lock over the link:
yarn.lock becomes a regular file (git shows a typechange), and the link is gone.
- The link's target, the lock the other consumers actually use, keeps the upstream registry entry, so they install unpatched bytes.
--dry-run predicts no refusal or warning (exit 0).
rollback restores the right bytes but writes them as a regular file, so the link is never restored.
Yarn itself writes through the link: after adding a dependency, yarn install (1.22.22) leaves yarn.lock a symlink and updates the target. The same happens with npm's package-lock.json in vendored mode (exit 0, link replaced), so this looks like a shared npm-family vendored gap rather than something specific to the yarn rewriter.
Impact
This is a refusal that should fire and doesn't. The project silently stops sharing its lock, and every other checkout or build that reads the link's target keeps the vulnerable package while this project's vex attests the patch. Undoing it with rollback / vendor --revert doesn't repair the link either.
Repro (Linux, any yarn 1.x; a local mock patch API at :8765)
mkdir -p shared p && cd p
echo '{"name":"a","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0"}}' > package.json
yarn install && mv yarn.lock ../shared/ && ln -s ../shared/yarn.lock yarn.lock
cp ../shared/yarn.lock ../orig
API="--api-url http://127.0.0.1:8765 --org org --api-token fake"
socket-patch scan --mode hosted $API --json --yes; echo $? # 1, redirect_symlinked_file_unsupported, nothing written
socket-patch scan --mode vendored $API --json --yes; echo $? # 0, status success
ls -l yarn.lock # -rw-r--r-- regular file, link gone
cmp ../shared/yarn.lock ../orig && echo "shared lock still unpatched"
socket-patch rollback $API --json # exit 0; yarn.lock is still a regular file
Expected vs actual
- Expected: the vendored arm refuses a symlinked write target before writing anything, as hosted does (CLI_CONTRACT "Takeover symlink pre-check" and the
redirect_symlinked_file_unsupported refusal; the bun binary lock "A symlinked binary write target is redirect_symlinked_file_unsupported (exit 1, including dry-run)"; uv vendored has pypi_uv_symlink_unsupported). The group commit's own crash recovery already refuses to "write through a symbolic link" (CLI_CONTRACT "Vendored group commit"). The other option is to write through the link like yarn does. Either way it shouldn't silently replace the link.
- Actual: vendored exits 0 and replaces the link with a regular file, the target stays unpatched, the dry-run gives no signal, and rollback doesn't restore the link.
Matrix (main 045d7ec, Linux, Node 22; each cell run twice)
| yarn |
hosted |
vendored dry-run |
vendored |
target lock |
rollback |
| 1.7.0 |
refuses (exit 1) |
exit 0, no warning |
exit 0, link → regular file |
unpatched |
bytes restored, link not restored |
| 1.10.1 |
refuses |
exit 0 |
same |
unpatched |
same |
| 1.22.22 |
refuses |
exit 0 |
same |
unpatched |
same |
npm 10 (package-lock.json, for comparison) |
— |
— |
exit 0, link → regular file |
— |
— |
This is OS-independent filesystem logic, so it wasn't probed on macOS or Windows. The code is unchanged since v5.0.
Suspect code
crates/socket-patch-core/src/utils/group_commit.rs:676 (apply_durably) and apply_deferred just below it: atomic_write_bytes* renames over the path without checking symlink_metadata. Only recovery (crosses_symlink, group_commit.rs:909) checks.
- Hosted has the gate:
crates/socket-patch-core/src/hosted/engine.rs:1532 (view.is_symlink → symlink_refusal). The vendored yarn/npm arms have no equivalent.
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
When
yarn.lockis a symbolic link (a shared lock in a monorepo or Docker build context, e.g.yarn.lock -> ../shared/yarn.lock),scan --mode hostedrefuses it withredirect_symlinked_file_unsupported("…an atomic rename, which would replace the link…; nothing was written", exit 1).scan --mode vendoredon the same project has no such gate. It exits 0 withstatus: "success", and its group commit renames the rewritten lock over the link:yarn.lockbecomes a regular file (git shows a typechange), and the link is gone.--dry-runpredicts no refusal or warning (exit 0).rollbackrestores the right bytes but writes them as a regular file, so the link is never restored.Yarn itself writes through the link: after adding a dependency,
yarn install(1.22.22) leavesyarn.locka symlink and updates the target. The same happens with npm'spackage-lock.jsonin vendored mode (exit 0, link replaced), so this looks like a shared npm-family vendored gap rather than something specific to the yarn rewriter.Impact
This is a refusal that should fire and doesn't. The project silently stops sharing its lock, and every other checkout or build that reads the link's target keeps the vulnerable package while this project's
vexattests the patch. Undoing it withrollback/vendor --revertdoesn't repair the link either.Repro (Linux, any yarn 1.x; a local mock patch API at :8765)
Expected vs actual
redirect_symlinked_file_unsupportedrefusal; the bun binary lock "A symlinked binary write target isredirect_symlinked_file_unsupported(exit 1, including dry-run)"; uv vendored haspypi_uv_symlink_unsupported). The group commit's own crash recovery already refuses to "write through a symbolic link" (CLI_CONTRACT "Vendored group commit"). The other option is to write through the link like yarn does. Either way it shouldn't silently replace the link.Matrix (main
045d7ec, Linux, Node 22; each cell run twice)package-lock.json, for comparison)This is OS-independent filesystem logic, so it wasn't probed on macOS or Windows. The code is unchanged since v5.0.
Suspect code
crates/socket-patch-core/src/utils/group_commit.rs:676(apply_durably) andapply_deferredjust below it:atomic_write_bytes*renames over the path without checkingsymlink_metadata. Only recovery (crosses_symlink,group_commit.rs:909) checks.crates/socket-patch-core/src/hosted/engine.rs:1532(view.is_symlink→symlink_refusal). The vendored yarn/npm arms have no equivalent.