Repository navigation
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Consider a project whose root requirements.txt pins a package only through a -r include outside the project root (for example -r ../shared/base.txt holding six==1.16.0), and which also has a pip lock pylock.toml. In that project, scan --mode vendored (and the hosted → vendored eject) wires the package in pylock.toml only. It leaves the out-of-root pin on PyPI and exits 0. Every follow-up check then calls the project patched:
- The scan reports
appliedwith apypi_multiple_lockfilesskip ("wiring pylock.toml; installs driven by requirements.txt retain their existing sources"), and exits 0. vendor --checkprintspkg:pypi/six@1.16.0: committed artifact and wiring verifiedand exits 0.vex, run against a venv installed withpip install -r requirements.txt(unpatched six), attestspkg:pypi/six@1.16.0asnot_affectedand exits 0. Its only hint is "the installed tree does not match its vendored artifact … re-run your package manager's install to resync it". That advice is wrong: reinstalling from requirements.txt installs the upstream wheel again.
Two related layouts behave correctly:
- No pylock.toml: the same layout is refused with
pypi_requirements_outside_root("six is pinned in a requirements include outside the project root, which vendor cannot edit …"), exit 1. - In-root include or root file that pylock can't co-wire: an in-root pin the wiring can't express (
six[foo]==1.16.0, the Hosted → vendored over an extras pin (six[foo]==X) in requirements.txt beside apip lockpylock.toml still un-hosts requirements.txt and leaves it unpatched, exit 0 (#1758 residual) #1794 shape) is reported as contested.vendor --checkexits 1 withwiring contested: … requirements.txt resolves the same version from elsewhere, andvexomits six.
So the out-of-root include gets past both the planner's refusal and discovery's contest check.
Impact
Any pip install -r requirements.txt consumer (CI, Dockerfiles) of a monorepo-style project that shares a base requirements file from a parent directory stays on the vulnerable release. Meanwhile the CLI, vendor --check and the VEX document all report it as patched. The false VEX attestation is the serious part.
Repro
This uses the repo's usual patch-API mock: batch / by-package / view / package grant pkg:pypi/six@1.16.0 and pkg:pypi/idna@3.7, and each artifact is the real PyPI wheel with SAFE = True appended. idna and certifi come from real PyPI. Real pip 26.2.2 / CPython 3.12.
mkdir -p shared app && cd app
printf 'six==1.16.0\n' > ../shared/base.txt
printf -- '-r ../shared/base.txt\nidna==3.7\ncertifi==2024.8.30\n' > requirements.txt
python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/pip lock -r requirements.txt -o pylock.toml
VIRTUAL_ENV=$PWD/.venv socket-patch scan --mode vendored --yes # exit 0
# skipped pkg:pypi/six@1.16.0 pypi_multiple_lockfiles: "wiring pylock.toml; installs driven by requirements.txt retain their existing sources"
# requirements.txt: idna line vendored; ../shared/base.txt untouched (six==1.16.0)
# pylock.toml: six and idna -> .socket/vendor/pypi/<uuid>/...whl
socket-patch vendor --check # exit 0, "six: committed artifact and wiring verified"
python -m venv fresh && fresh/bin/pip install --no-cache-dir -r requirements.txt
fresh/bin/python -c 'import six; print(hasattr(six, "SAFE"))' # False (upstream PyPI wheel)
VIRTUAL_ENV=$PWD/fresh socket-patch vex --product pkg:pypi/app@1 -O vex.json # exit 0
# statements[0].status = not_affected, subcomponents = [idna@3.7, six@1.16.0]Control: delete pylock.toml first, and the same scan --mode vendored fails six with pypi_requirements_outside_root (exit 1).
Expected vs actual
- Expected: this layout gets the same refusal as the no-pylock case. The planner's own comment on
pypi_requirements_outside_rootsays: "an include outside the project root cannot be edited by a committable vendor flow; rewriting only the in-root copy would leave pip a duplicate requirement. Fail closed." A pylock beside it doesn't change whatpip install -r requirements.txtinstalls. Failing that, discovery should contest the pylock wiring, as it does for an in-root pin (CLI_CONTRACT.md "Contested locks": a lock that resolves the same version from elsewhere contests the wiring,vendor --checkfails andvexomits it).vexmust not attestnot_affectedfor an install source that delivers the upstream release. - Actual: exit 0 and
vendor --checkexit 0.vexattestsnot_affectedwhile pip installs unpatched six.
Matrix (Linux; the code paths are OS-independent)
| pip / Python | mode | requirements.txt shape | scan | vendor --check |
pip install -r six |
vex six |
|---|---|---|---|---|---|---|
| 26.2.2 / 3.12 | vendored | -r ../shared/base.txt (out of root) + pylock |
0 | 0 (verified) | unpatched | not_affected (reproduced 3/3) |
| 26.2.2 / 3.12 | hosted → vendor (eject) |
same | 0 | 0 | unpatched | not_affected |
| 25.1 / 3.12 | vendored | same | 0 | 0 | unpatched | not_affected |
| 26.2.2 / 3.12 | vendored | same, no pylock | 1 pypi_requirements_outside_root |
n/a | unpatched (refused, fine) | not attested |
| 26.2.2 / 3.12 | vendored | in-root six[foo]==1.16.0 + pylock (#1794 shape) |
0 | 1 (contested) | unpatched | omitted (correct) |
| 26.2.2 / 3.12, 25.1 / 3.13 | hosted | out-of-root include + pylock | 0, redirect_requirements_entry_not_found |
n/a | unpatched | not attested (correct) |
Tested on main c07af61 (v5.0.1 is the latest release). No first-bad bisect: the out-of-root filter in discovery dates from the include walk itself.
Suspect code
crates/socket-patch-core/src/vendor/pypi_requirements.rs:1231(requirements_include_names_in):if !is_in_root_rel(rel) { return Ok(false); }drops out-of-root includes from the list thatvex/discover/pypi_other.rs:240(extract_requirements) reads. Discovery therefore never sees the registry pin that contests the pylock wiring. The module doc atpypi_other.rs:61("every in-root-rinclude") makes this explicit, but an out-of-root include is still something pip installs.crates/socket-patch-core/src/vendor/pypi.rs:395/:429(detect_pypi_flavor):sibling_pin_wirable(pypi_requirements.rs:427) is false for the read-only include, so requirements.txt is pushed as apypi_multiple_lockfilesloser with a warning. The requirements-only planner instead raisespypi_requirements_outside_root(pypi_requirements.rs:893).- Related: Hosted → vendored over an extras pin (
six[foo]==X) in requirements.txt beside apip lockpylock.toml still un-hosts requirements.txt and leaves it unpatched, exit 0 (#1758 residual) #1794 / PR Fix extras pin un-hosted beside a pylock (#1794) #1796 add a refusal for an extras pin in the same loser path (fresh_sibling_pin), but they don't cover a non-editable out-of-root include.
No probe runs: this routine's probe branches are still blocked, so this was reproduced on Linux only.
Activity
mikolalysenko commentedon Oct 29, 2026
[agent] Triaged: priority:p1 (pip). This is a false VEX attestation. It's related to #1794 / PR #1796 (the same
pypi_multiple_lockfilesloser path indetect_pypi_flavor), but it isn't covered there: #1796 only refuses extras pins. Here an out-of-root-rinclude is pushed as a loser, and discovery drops out-of-root includes, so nothing contests the pylock wiring.[agent] Claiming this issue (shared root cause: the vendored PyPI lock wiring and discovery ignore a pin that pip installs from an out-of-root
-rinclude). Branch: agent/fix-pypi-out-of-root-include-pylock. Claim-ID: 2026-10-29T20:20:56Z-b35064Generated by Claude Code
mikolalysenko commentedon Oct 29, 2026
[agent] Draft PR: #1853
Generated by Claude Code
Refuse lock wiring beside an out-of-root -r pin
Contest lock wiring with out-of-root -r pins
Test vendor beside an out-of-root -r pin
Cover every pip shape in out-of-root includes
Refuse bare url and path lines outside the root
Refuse an unreadable include outside the root
mikolalysenko commentedon Oct 30, 2026
[agent] pip bug-hunt (ledger #309), new information: an absolute-path include (
-r /abs/path/shared/base.txtholdingsix==1.16.0) beside apip lockpylock.toml reproduces the same outcome on main5ca5623(pip 26.2.2 / CPython 3.12).scan --mode vendoredexits 0 with thepypi_multiple_lockfilesskip,vendor --checkexits 0, a freshpip install -rgets unpatched six, andvexattests sixnot_affected.PR #1853's head (
70b2147) handles it, verified with real pip:scan --mode vendoredvendor --checkon the project main vendoredvexsix5ca5623pypi_requirements_outside_root, pylock untouched../shared/base.txtpypi_requirements_outside_rootA nit for the PR, not a blocker: the contested message ends "delete whichever of the two locks the project does not install from", but
base.txtisn't a lock.Generated by Claude Code
Merge main into fix-pypi-requirement-env-expand
Contest ${VAR} pins in out-of-root includes too