[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Lockfile-only discovery (a checkout with no venv yet, which is the usual CI case) reads requirements.txt through utils::requirements::exact_pin. That function takes the first whitespace-separated token of the line and splits it on ==. So any exact pin with whitespace around == is not seen as a pin: six == 1.15.0, six ==1.15.0, six== 1.15.0, six[x] == 1.15.0, and the legacy parenthesised form six (==1.15.0). pip 20.3.4 and 26.2.1 install every one of these forms.
The package never reaches the patch API. scan (hosted or vendored) prints "No patches available for installed packages.", exits 0 with status: success, and leaves requirements.txt untouched. The next pip install -r requirements.txt installs the unpatched release.
The hosted rewriter itself handles these forms. Once a venv with the package exists, the same file is rewritten correctly (six == 1.17.0 → six @ <hosted url>#sha256=…) and installs PATCHED. Only the lockfile-only inventory misses them.
Impact
A fresh checkout (CI, Docker build, new clone) whose hand-written requirements.txt spaces its pins silently gets no patches, and nothing warns about it. Vendored mode is affected the same way, because discovery runs before vendoring.
Repro (Linux, main 61cfb9b)
# mock patch API for pkg:pypi/six@1.17.0 on :8766 (batch / by-package / view / package / patched wheel)
mkdir p && cd p
printf 'six == 1.17.0\nidna==3.7\n' > requirements.txt
socket-patch scan --mode hosted --json --api-url http://127.0.0.1:8766 --api-token fake --org test-org --yes
# exit 0, {"status":"success"}, "No patches available"; the batch request carries no six purl
cat requirements.txt # unchanged
python3.12 -m venv .venv && .venv/bin/pip install -q -r requirements.txt
.venv/bin/python -c 'import six; print(getattr(six, "SOCKET_PATCHED", 0))' # 0 → UNPATCHED
# control: same file, now with the venv present
socket-patch scan --mode hosted ... # "Switched 1 package to hosted patches; rewrote 1 file."
# fresh venv + pip install -r → PATCHED
# control: `six==1.17.0` (no spaces), lockfile-only → discovered, rewritten, PATCHED
Every form I tried behaves the same way: six == 1.17.0, six ==1.17.0, six== 1.17.0, six[x] == 1.17.0 and six (==1.17.0). scan --mode vendored on a lockfile-only checkout also prints "No patches available" for six == 1.17.0, while six==1.17.0 finds the patch.
Expected vs actual
- Expected: docs/ecosystems.md lists requirements.txt as supported for hosted and vendored, including lock-only checkouts. pip treats
six == 1.15.0 exactly like six==1.15.0, and so does socket-patch's own hosted rewriter (patch/redirect/requirements.rs requirement_version trims the specifier). So the lock-only scan should discover pkg:pypi/six@1.15.0 and rewrite the line.
- Actual: the package is invisible to lockfile-only discovery. The scan exits 0 with "No patches available" and doesn't warn.
OS × version matrix
Probe run https://github.com/SocketDev/socket-patch/actions/runs/36955168379. The patch is for six 1.15.0, which isn't installed anywhere on the runners. Each cell is a lockfile-only scan --mode hosted, then a fresh-venv pip install -r.
| OS |
Python / pip |
six==1.15.0 |
six == 1.15.0 |
six ==1.15.0 |
six== 1.15.0 |
six[x] == 1.15.0 |
six (==1.15.0) |
| ubuntu-latest |
3.8 / 20.3.4 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
| ubuntu-latest |
3.13 / 26.2.1 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
| macos-latest |
3.8 / 20.3.4 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
| macos-latest |
3.13 / 26.2.1 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
| windows-latest |
3.8 / 20.3.4 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
| windows-latest |
3.13 / 26.2.1 |
rewritten, PATCHED |
not discovered, UNPATCHED |
same |
same |
same |
same |
Locally (Linux, pip 24.0 / py3.12), it reproduced twice on main 61cfb9b with six 1.17.0.
First bad version
This is not a regression: v4.0.0 from PyPI behaves the same way (six == 1.17.0 lockfile-only → "API query complete", file unchanged).
Suspect code
crates/socket-patch-core/src/utils/requirements.rs:101: exact_pin does code.split(';').next()?.split_whitespace().next()? and then split_once("=="). For six == 1.15.0 the first token is six, with no ==; for six== 1.15.0 the version is empty.
crates/socket-patch-core/src/vendor/lock_inventory/pypi.rs:611: the lock inventory calls exact_pin and drops anything else that isn't a name @ <socket url> reference.
vex/discover/pypi_other.rs imports the same exact_pin, so lock-only vex evidence probably has the same gap.
Related but distinct: #412 (pins in -r includes are skipped) and #475 (PEP 440-equivalent versions like six==1.16). #478 doesn't touch exact_pin.
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Lockfile-only discovery (a checkout with no venv yet, which is the usual CI case) reads requirements.txt through
utils::requirements::exact_pin. That function takes the first whitespace-separated token of the line and splits it on==. So any exact pin with whitespace around==is not seen as a pin:six == 1.15.0,six ==1.15.0,six== 1.15.0,six[x] == 1.15.0, and the legacy parenthesised formsix (==1.15.0). pip 20.3.4 and 26.2.1 install every one of these forms.The package never reaches the patch API.
scan(hosted or vendored) prints "No patches available for installed packages.", exits 0 withstatus: success, and leaves requirements.txt untouched. The nextpip install -r requirements.txtinstalls the unpatched release.The hosted rewriter itself handles these forms. Once a venv with the package exists, the same file is rewritten correctly (
six == 1.17.0→six @ <hosted url>#sha256=…) and installs PATCHED. Only the lockfile-only inventory misses them.Impact
A fresh checkout (CI, Docker build, new clone) whose hand-written requirements.txt spaces its pins silently gets no patches, and nothing warns about it. Vendored mode is affected the same way, because discovery runs before vendoring.
Repro (Linux, main
61cfb9b)Every form I tried behaves the same way:
six == 1.17.0,six ==1.17.0,six== 1.17.0,six[x] == 1.17.0andsix (==1.17.0).scan --mode vendoredon a lockfile-only checkout also prints "No patches available" forsix == 1.17.0, whilesix==1.17.0finds the patch.Expected vs actual
six == 1.15.0exactly likesix==1.15.0, and so does socket-patch's own hosted rewriter (patch/redirect/requirements.rsrequirement_versiontrims the specifier). So the lock-only scan should discoverpkg:pypi/six@1.15.0and rewrite the line.OS × version matrix
Probe run https://github.com/SocketDev/socket-patch/actions/runs/36955168379. The patch is for six 1.15.0, which isn't installed anywhere on the runners. Each cell is a lockfile-only
scan --mode hosted, then a fresh-venvpip install -r.six==1.15.0six == 1.15.0six ==1.15.0six== 1.15.0six[x] == 1.15.0six (==1.15.0)Locally (Linux, pip 24.0 / py3.12), it reproduced twice on main
61cfb9bwith six 1.17.0.First bad version
This is not a regression: v4.0.0 from PyPI behaves the same way (
six == 1.17.0lockfile-only → "API query complete", file unchanged).Suspect code
crates/socket-patch-core/src/utils/requirements.rs:101:exact_pindoescode.split(';').next()?.split_whitespace().next()?and thensplit_once("=="). Forsix == 1.15.0the first token issix, with no==; forsix== 1.15.0the version is empty.crates/socket-patch-core/src/vendor/lock_inventory/pypi.rs:611: the lock inventory callsexact_pinand drops anything else that isn't aname @ <socket url>reference.vex/discover/pypi_other.rsimports the sameexact_pin, so lock-onlyvexevidence probably has the same gap.Related but distinct: #412 (pins in
-rincludes are skipped) and #475 (PEP 440-equivalent versions likesix==1.16). #478 doesn't touchexact_pin.