[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
After a hosted or vendored Composer run, socket-patch tells the user which installed directory to delete before composer install so Composer reinstalls the patched bytes. Both hints hardcode vendor/<vendor>/<name>. They ignore the project's config.vendor-dir, and COMPOSER_VENDOR_DIR, even though the crawler resolves both (resolve_local_vendor_dir). In a project with "config": {"vendor-dir": "lib"}, the user deletes a path that doesn't exist, composer install reports "Nothing to install", and the installed package stays unpatched.
- Hosted, every Composer version (dist-only lock entry):
first remove vendor/acme/tool — Composer does not reinstall a package whose lock entry has no source…. The package is really in lib/acme/tool.
- Vendored, Composer 1:
Composer 1 does not reinstall a locked package whose dist changed: remove vendor/acme/tool first, then run composer install.
Impact
- Hosted: the installed tree stays unpatched.
vex does catch this and omits the package as not_applied, so there's no false attestation, but the CLI's own remediation step fails.
- Vendored + Composer 1: the installed tree stays unpatched, and
vex still emits not_affected. It warns that the live tree differs, which is the documented vendored posture, so the only cue is that warning. The user did exactly what the CLI told them to do.
Repro (Linux, PHP 8.3, main 045d7ec)
A dist-only package from an inline package repository, plus a mock patch API (batch, patches/package granted with sha1 + sha512, view/<uuid> and the archive route, the same shape as crates/socket-patch-cli/tests/e2e_redirect_composer_build.rs).
mkdir app && cd app
cat > composer.json <<EOF
{"name":"acme/app",
"repositories":[{"type":"package","package":{"name":"acme/tool","version":"1.0.0",
"dist":{"type":"zip","url":"file://$PWD/../tool-1.0.0.zip"},"autoload":{"files":["src/run.php"]}}}],
"require":{"acme/tool":"1.0.0"},
"config":{"vendor-dir":"lib"}}
EOF
composer update -n # installs lib/acme/tool (dist, no source)
socket-patch scan --mode hosted --yes --api-url http://127.0.0.1:18765 --api-token fake --org acme
# 2. Reinstall … (e.g. `composer install`; first remove vendor/acme/tool — …)
rm -rf vendor/acme/tool # as instructed: path does not exist
composer install -n # "Nothing to install, update or remove"
tail -1 lib/acme/tool/src/run.php # still the original bytes
socket-patch vex --output v.json … # "omitting pkg:composer/acme/tool@1.0.0 … (not_applied)"
The vendored variant on Composer 1.10.28: scan --mode vendored prints remove vendor/acme/tool first. After that, composer install reports "Nothing to install or update", lib/acme/tool is pristine, and vex writes not_affected with the "installed tree does not match its vendored artifact" warning.
Control: the identical project with vendor-dir: vendor (or unset) gets a hint that names the right directory, and following it installs the patched bytes on every cell below.
Expected vs actual
- Expected: docs/testing/composer-compatibility.md, "Reinstalling over an existing
vendor/", says to remove the installed package directory, and that scan --mode hosted / vendor / scan --mode vendored "print both instructions". The printed path should be the directory Composer actually installed the package to, meaning the resolved vendor dir (COMPOSER_VENDOR_DIR → config.vendor-dir → vendor, as crawlers/composer_crawler.rs already resolves it), or the install-path from installed.json for composer/installers packages.
- Actual: the hint is always
vendor/<vendor>/<name>.
Matrix (each cell run twice, both times the same result)
| OS |
Composer |
Mode |
vendor-dir: vendor |
vendor-dir: lib |
| Linux |
2.10.3 |
hosted (dist-only entry) |
hint correct, patched |
hint wrong, stays unpatched, vex omits |
| Linux |
2.2.30 |
hosted (dist-only entry) |
hint correct, patched |
hint wrong, stays unpatched, vex omits |
| Linux |
1.10.28 |
hosted |
hint correct, patched |
hint names vendor/acme/tool (removing lib/acme/tool by hand works) |
| Linux |
1.10.28 |
vendored |
hint correct, patched |
hint wrong, stays unpatched, vex not_affected with a warning |
The bug is pure string formatting, so it's OS-independent (macOS/Windows weren't probed). It's been present since the hints were added in #358 (de316b4).
Suspect code
crates/socket-patch-cli/src/commands/composer_hints.rs:52: format!("vendor/{p}") in vendored_reinstall_hints
crates/socket-patch-cli/src/commands/composer_hints.rs:77: format!("vendor/{}", …) in hosted_reinstall_hint, plus the literal vendor/<vendor>/<name> text at lines 82–92
- The resolver to reuse:
resolve_local_vendor_dir in crates/socket-patch-core/src/crawlers/composer_crawler.rs:501
Related: the composer/installers note on #463 (the vendored hint names the wrong dir for installer-paths packages) has the same root cause.
[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
After a hosted or vendored Composer run, socket-patch tells the user which installed directory to delete before
composer installso Composer reinstalls the patched bytes. Both hints hardcodevendor/<vendor>/<name>. They ignore the project'sconfig.vendor-dir, andCOMPOSER_VENDOR_DIR, even though the crawler resolves both (resolve_local_vendor_dir). In a project with"config": {"vendor-dir": "lib"}, the user deletes a path that doesn't exist,composer installreports "Nothing to install", and the installed package stays unpatched.first remove vendor/acme/tool — Composer does not reinstall a package whose lock entry has no source…. The package is really inlib/acme/tool.Composer 1 does not reinstall a locked package whose dist changed: remove vendor/acme/tool first, then run composer install.Impact
vexdoes catch this and omits the package asnot_applied, so there's no false attestation, but the CLI's own remediation step fails.vexstill emitsnot_affected. It warns that the live tree differs, which is the documented vendored posture, so the only cue is that warning. The user did exactly what the CLI told them to do.Repro (Linux, PHP 8.3, main
045d7ec)A dist-only package from an inline
packagerepository, plus a mock patch API (batch,patches/packagegranted with sha1 + sha512,view/<uuid>and the archive route, the same shape ascrates/socket-patch-cli/tests/e2e_redirect_composer_build.rs).The vendored variant on Composer 1.10.28:
scan --mode vendoredprintsremove vendor/acme/tool first. After that,composer installreports "Nothing to install or update",lib/acme/toolis pristine, andvexwritesnot_affectedwith the "installed tree does not match its vendored artifact" warning.Control: the identical project with
vendor-dir: vendor(or unset) gets a hint that names the right directory, and following it installs the patched bytes on every cell below.Expected vs actual
vendor/", says to remove the installed package directory, and thatscan --mode hosted/vendor/scan --mode vendored"print both instructions". The printed path should be the directory Composer actually installed the package to, meaning the resolved vendor dir (COMPOSER_VENDOR_DIR→config.vendor-dir→vendor, ascrawlers/composer_crawler.rsalready resolves it), or theinstall-pathfrominstalled.jsonfor composer/installers packages.vendor/<vendor>/<name>.Matrix (each cell run twice, both times the same result)
vendor-dir: vendorvendor-dir: libvendor/acme/tool(removinglib/acme/toolby hand works)not_affectedwith a warningThe bug is pure string formatting, so it's OS-independent (macOS/Windows weren't probed). It's been present since the hints were added in #358 (
de316b4).Suspect code
crates/socket-patch-cli/src/commands/composer_hints.rs:52:format!("vendor/{p}")invendored_reinstall_hintscrates/socket-patch-cli/src/commands/composer_hints.rs:77:format!("vendor/{}", …)inhosted_reinstall_hint, plus the literalvendor/<vendor>/<name>text at lines 82–92resolve_local_vendor_dirincrates/socket-patch-core/src/crawlers/composer_crawler.rs:501Related: the composer/installers note on #463 (the vendored hint names the wrong dir for
installer-pathspackages) has the same root cause.