Skip to content

Lockfile-only requirements.txt discovery skips exact pins written with spaces (six == 1.15.0, six (==1.15.0)), so a fresh checkout reports "No patches available" and pip installs the unpatched release #523

Description

[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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions