Skip to content

Commit af4e81e

Browse files
committed
pip bug-hunt run 50
1 parent 2de62ab commit af4e81e

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

‎entries/pip/20261028T203000Z.md‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
[agent] 2026-10-28 (20:30 UTC): pip / requirements.txt bug-hunt run
2+
3+
**Tested:** main `b4b6d98` (21 commits since `b787454`, including #1759 and #1776), latest release v5.0.1. Linux only: real pip 26.2.2 / CPython 3.11, lock-only checkouts with a `pip lock` pylock. Release binary built from main.
4+
5+
**Probe branches:** none (branch deletion is still blocked).
6+
7+
## Re-triage
8+
9+
- #1758 (closed by #1759): **verified fixed** with real pip. Comment: https://github.com/SocketDev/socket-patch/issues/1758#issuecomment-6479292634
10+
- #977 / #638: not re-run (no related code changed).
11+
12+
## Cells (Linux, pip 26.2.2)
13+
14+
| Cell | Result |
15+
| --- | --- |
16+
| hosted → `vendor` (eject), `idna==3.7` + `six==1.16.0` + pylock.toml | pass (both wired, pip installs patched from both, `vendor --check` ok) |
17+
| same via `scan --mode vendored --yes` | pass |
18+
| `-r req/base.txt` include + pylock, `scan --mode vendored`, then `vendor --revert` | pass (include wired; revert byte-identical) |
19+
| same, `scan --mode hosted` | `redirect_unattributable`, nothing written: known non-bug class (hosted rewrites root only) |
20+
| pip-compile CRLF, two `--hash` + `# via`, `pylock.dev.toml`, eject then revert | pass (CRLF kept, hash on the path line, revert re-hashes from PyPI) |
21+
| marker pin `six==1.16.0 ; python_version >= "3"` + pylock, eject | pass |
22+
| extras pin `six[foo]==1.16.0` + pylock, eject / `scan --mode vendored` | **fail → #1794** (requirements.txt un-hosted to PyPI, only pylock wired, exit 0, `vendor --check` exit 1; 3/3) |
23+
| extras pin, requirements.txt only, eject | pass (`pypi_extras_unsupported`, hosted pin kept, exit 1) |
24+
25+
## Issues
26+
27+
- Filed #1794: https://github.com/SocketDev/socket-patch/issues/1794
28+
- Commented on #1758 (verification).
29+
30+
## False positives ruled out
31+
32+
- The eject refused with "the lock's packages record no `index` and carry files from 127.0.0.1" because the mock served the unpatched wheels, so pip lock recorded mock URLs. The fix was to take the unpatched wheels from real PyPI. That refusal is correct.
33+
- `six>=1.16` / bare `six` cells: pip lock resolved six 1.17.0 from PyPI, while the mock grants 1.16.0. That's a harness mismatch, not a finding.
34+
- `pip install -r pylock.toml` failing with "= is not a valid operator": the fresh venv's pip upgrade had gone to the mock index and failed, which left pip 24. Already a known pip-version limit.
35+
36+
## Harness (rebuild in about 5 minutes)
37+
38+
Make the patched wheel from the real `six-1.16.0-py2.py3-none-any.whl`: append `\nSAFE = True\n` to `six.py` and recompute RECORD. Then run the mock below with `python3 -I mock.py 8765 <dir>` (`<dir>/dl/` holds the upstream wheels and `<dir>/patched.whl` the patched one), and set these env vars: `SOCKET_API_URL=SOCKET_PATCH_SERVER_URL=SOCKET_VENDOR_URL=http://127.0.0.1:8765 SOCKET_API_TOKEN=fake SOCKET_ORG_SLUG=o SOCKET_VENDOR_SOURCE=service`. Leave `PIP_INDEX_URL` and `SOCKET_PYPI_JSON_API` unset (real PyPI) for pylock cells. Don't `pkill -f mock.py` from the same shell, because it kills that shell.
39+
40+
<details><summary>mock.py</summary>
41+
42+
```python
43+
import http.server,json,hashlib,base64,sys,os,subprocess
44+
PORT=int(sys.argv[1]); D=sys.argv[2]
45+
ORG='o'; UUID='2b1f6c1e-8d3a-4f6b-9c2d-7e5a9b1c3d11'; PURL='pkg:pypi/six@1.16.0'; W='six-1.16.0-py2.py3-none-any.whl'
46+
B=f'http://127.0.0.1:{PORT}'
47+
orig=open(f'{D}/dl/{W}','rb').read(); pat=open(f'{D}/patched.whl','rb').read()
48+
import zipfile,io
49+
def member(b): return zipfile.ZipFile(io.BytesIO(b)).read('six.py')
50+
def gsha(b): return hashlib.sha256(b'blob %d\0'%len(b)+b).hexdigest()
51+
o6,p6=member(orig),member(pat)
52+
HROUTE=f'/patch/pypi/six/1.16.0/33333333-3333-4333-8333-333333333333/{UUID}/{W}'
53+
view={"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","files":{"six.py":{"beforeHash":gsha(o6),"afterHash":gsha(p6),"blobContent":base64.b64encode(p6).decode()}},"vulnerabilities":{},"description":"x","license":"MIT","tier":"free"}
54+
class H(http.server.BaseHTTPRequestHandler):
55+
def log_message(self,*a): open(f'{D}/mock.log','a').write(self.command+' '+self.path+'\n')
56+
def send(self,code,body,ct='application/json'):
57+
if not isinstance(body,bytes): body=json.dumps(body).encode()
58+
self.send_response(code); self.send_header('Content-Type',ct); self.send_header('Content-Length',str(len(body))); self.end_headers(); self.wfile.write(body)
59+
def do_POST(self):
60+
n=int(self.headers.get('Content-Length',0)); self.rfile.read(n); p=self.path.split('?')[0]
61+
if p.endswith('/patches/batch'):
62+
return self.send(200,{"packages":[{"purl":PURL,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":[],"ghsaIds":[],"severity":"high","title":"t"}]}],"canAccessPaidPatches":False})
63+
if p.endswith('/patches/package'):
64+
u=B+HROUTE
65+
return self.send(200,{"results":{UUID:{"status":"granted","url":u,"purl":PURL,"artifacts":[{"kind":"tarball","url":u,"integrity":{"sha256":hashlib.sha256(pat).hexdigest(),"sha512":"sha512-"+base64.b64encode(hashlib.sha512(pat).digest()).decode()}}],"registryOverride":None}}})
66+
self.send(404,{})
67+
def do_GET(self):
68+
p=self.path.split('?')[0]
69+
if '/patches/by-package/' in p:
70+
return self.send(200,{"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{}}],"canAccessPaidPatches":False})
71+
if p.endswith(f'/patches/view/{UUID}'): return self.send(200,view)
72+
if p==HROUTE: return self.send(200,pat,'application/octet-stream')
73+
if p=='/pypi/six/1.16.0/json':
74+
return self.send(200,{"info":{"name":"six","version":"1.16.0"},"urls":[{"filename":W,"url":f"{B}/files/{W}","digests":{"sha256":hashlib.sha256(orig).hexdigest()},"size":len(orig),"packagetype":"bdist_wheel","upload_time_iso_8601":"2021-05-05T14:18:17.000000Z"}]})
75+
if p.startswith('/files/') and os.path.exists(f'{D}/dl/'+p[7:]): return self.send(200,open(f'{D}/dl/'+p[7:],'rb').read(),'application/octet-stream')
76+
if p.startswith('/simple/'):
77+
n=p.split('/')[2].lower().replace('_','-').replace('.','-')
78+
fs=[f for f in os.listdir(f'{D}/dl') if f.split('-')[0].lower().replace('_','-').replace('.','-')==n]
79+
if not fs: return self.send(404,{})
80+
return self.send(200,('<html><body>'+''.join(f'<a href="{B}/files/{f}#sha256={hashlib.sha256(open(f"{D}/dl/"+f,"rb").read()).hexdigest()}">{f}</a>' for f in fs)+'</body></html>').encode(),'text/html')
81+
if p.startswith('/pypi/') and p.endswith('/json'):
82+
_,_,n,v,_=p.split('/')
83+
fs=[f for f in os.listdir(f'{D}/dl') if f.lower().startswith(n.lower().replace('-','_')+'-'+v+'-')]
84+
if fs:
85+
f=fs[0]; b=open(f'{D}/dl/'+f,'rb').read()
86+
return self.send(200,{"info":{"name":n,"version":v},"urls":[{"filename":f,"url":f"{B}/files/{f}","digests":{"sha256":hashlib.sha256(b).hexdigest()},"size":len(b),"packagetype":"bdist_wheel","upload_time_iso_8601":"2021-05-05T14:18:17.000000Z"}]})
87+
self.send(404,{})
88+
http.server.ThreadingHTTPServer(('127.0.0.1',PORT),H).serve_forever()
89+
```
90+
</details>
91+
92+
## Next
93+
94+
1. #1794: re-verify once fixed. Also check other shapes that `sibling_pin_wirable` rejects beside a pylock (a symlinked requirements.txt, an include outside the root).
95+
2. Run-49 follow-ups 1q: a two-package dotted-name vendored → hosted takeover in a CRLF `-r` include, and orphan wiring in `-c` / `requirements-dev.txt` under `rollback`.
96+
3. Re-verify the maintainer-closed fixes #1623, #1602, #1587 with real pip (backlog 1p).
97+
4. pip 20.3.4 / CPython 3.8 spot-check of the #1759 path (requirements.txt only, since pip 20 has no `pip lock`).
98+
5. A Windows / macOS probe once branch deletion works.

‎state/pip.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[agent] Progress ledger for the scheduled pip / requirements.txt bug-hunt routine (label pm:pip).
22

3-
Last run: 2026-10-28 13:48 UTC (forty-ninth run), main `b787454` (unchanged since run 47), latest release v5.0.1. Nothing new filed. New harness: a stand-alone Python mock of the patch API / vendoring service / PyPI JSON + simple index (no Rust test needed), driving the release binary with real pip. All cells pass on Linux, pip 26.x / CPython 3.11 and pip 20.3.4 / CPython 3.8: a `_`/`.`-named package (`Typing.Extensions`) through hosted (extras + marker, hashed continuation, CRLF pip-compile `# via` output), vendored (markers, trailing comment, hash continuation, revert byte-identical), `vex`, a `-c`-only pin, and #991 / #1739 unledgered-wiring refusals. Probe branches are still blocked. Open: #1758 (fix PR #1759), #977, #638 (+ #836 enhancement).
3+
Last run: 2026-10-28 ~20:30 UTC (fiftieth run), main `b4b6d98`, latest release v5.0.1. #1758 verified fixed by #1759 (real pip 26.2.2). Filed #1794: an extras pin (`six[foo]==X`) in requirements.txt beside a `pip lock` pylock is still un-hosted and left unpatched by the hosted → vendored eject / takeover (exit 0). Harness: the Python mock from run 49, rebuilt and inlined in entry 20261028T203000Z (unpatched wheels come from real PyPI so pylock records PyPI URLs; a 127.0.0.1 index makes the pylock restore refuse, which is correct). Probe branches are still blocked. Open: #1794, #977, #638 (+ #836 enhancement).
44
## Coverage matrix
55

66
| OS | pip / Python | agent (.venv) | agent egg-info | hosted (unhashed → fragment pin) | vendored | rollback / remove | vendored → hosted takeover | system-site venv | `-r` include, lock-only | PEP 440-equivalent pin (`==1.16`) | global `-g` (`--user`) | lock-only spaced pin (`six == X`) | hosted foreign direct ref (`six @ mirror`) | lock-only PEP 440-equivalent pin (`==1.16`) | no root requirements.txt (`--json`) | hosted → vendored takeover (`vendor`) | `vendor --dry-run` over hosted | `get --mode vendored` over hosted | vendored → hosted, include / transitive pin | UTF-16 requirements.txt (hosted / lock-only) | root file used as `-c` by a sibling | vendored re-vendor to a new uuid | vendored pin removed / bumped (`--prune`) | `pip lock` pylock.toml (hosted / vendored) | agent same-size patch vs `__pycache__` | agent rollback of a patch-added file in a new dir | socket.yml package specs (PyPI spelling) | hosted superseding re-pin (A → B) | `--max-new-patches` (hosted) | `get <name>` PyPI spelling | `--max-new-patches` (vendored) | vendored supersede under cap 0 (hashed include / CRLF marker) | vendored line comment edited, then revert | quoted / `${VAR}` `-r` include (lock-only) | option before `-r` on one line (lock-only) | hosted root pin + duplicate pin in `-r` include (vex / rollback) | PEP 263 coding-line (latin-1) requirements.txt, lock-only | `apply --check` (agent, #1029) | vendored `scan --prune` liveness (#1050) | hosted / vendored file regenerated by `pip freeze` | hashed hosted root + hashed `-r` include (`--require-hashes`) | dangling `\` continuation at EOF (lock-only) | lock-only bare-CR / FF / NEL / U+2028 line breaks | `list` (hosted / marker split / include) | requirements spelling contests Pipfile.lock wiring (vex) |
@@ -53,10 +53,13 @@ Run twenty-one (2026-10-05, pip 26.2.1 / py3.11, main `9c43dfc`): #858 atomic wr
5353

5454
Commands covered on Linux: scan (all modes), get (hosted, agent `-g`), rollback, remove, vendored takeover, vex (hosted with the mock origin, vendored, `-g`), repair, list, concurrent runs. Also covered (2026-10-02): free-threaded CPython 3.13t venvs, a `.venv` symlink to an out-of-tree venv, a 16-case hosted grammar sweep with fresh installs and rollbacks, virtualenv layouts, pip-tools `pip-compile --generate-hashes` / `pip-sync` over a hosted file plus rollback, `-c` constraints with an unpinned root, `pip install --target` trees, `--json` envelopes of rollback / remove failures, and interrupted (SIGTERM / SIGKILL) hosted scans. Run ten (2026-10-03, pip 26.2.1 / py3.13 and pip 20.3.4 / py3.8): an 18-shape hosted and vendored grammar sweep with fresh `pip install -r` plus rollback and reinstall, vendored lock-only `scan` over `-r` / `-rX` / `--requirement=` / CRLF / subdirectory includes, `-c` constraints, and grant-token re-pin: all pass or refuse explicitly. Run eight (pip 24.0 / py3.11 and pip 20.3.4 / py3.8): PEP 503 name normalisation (`_` / `-` / `.` / case, venv and lock-only), duplicate and marker-split pins, BOM, no trailing newline, `--no-index` + `--find-links`, per-line `--config-settings`, `--require-hashes` with multiple and sha384 hashes, `-e` / VCS neighbours (lock-only + `vex`), symlinked root and include files, out-of-root includes, `--prefix` trees: all pass or refuse explicitly.
5555

56+
Run 50 (Linux, pip 26.2.2 / CPython 3.11, lock-only + pylock): hosted → vendored eject and `scan --mode vendored` over requirements.txt + pylock.toml: exact pin pass, marker pin pass, `-r` include pin (vendored) pass, pip-compile CRLF `--hash` + `pylock.dev.toml` pass, `vendor --revert` byte-identical pass; extras pin **fail #1794**; hosted with the pin only in a `-r` include beside a pylock: `redirect_unattributable`, fail-closed (known non-bug class).
57+
5658
## Backlog
5759

5860
1q. Run 49 follow-ups: reuse the Python mock (entry 20261028T134832Z) for a two-package vendored → hosted takeover with dotted names in a CRLF `-r` include, and an orphan wire in a `-c` constraints file / `requirements-dev.txt` sibling under `rollback` (unscoped refusal).
59-
1o. #1758 follow-ups: re-verify once #1759 merges. Then a `-r` include holding the pin beside pylock, and `pylock.<name>.toml`. (The two-package eject refusal / rollback over requirements.txt alone passed on run 48; a failure that only surfaces after the restore, i.e. a true `eject_rolled_back`, wasn't reachable with the fixtures: a corrupt artifact is refused at the grant step.)
61+
1r. #1794: re-verify once fixed (eject and `scan --mode vendored`; also `--dry-run` previews). Check other `sibling_pin_wirable`-rejected shapes beside a pylock: a symlinked requirements.txt, an include outside the root, a pin whose package also has a vendor line for another version.
62+
1o. done 2026-10-28 run 50: #1758 verified fixed; the `-r` include and `pylock.<name>.toml` cells pass. (The two-package eject refusal / rollback over requirements.txt alone passed on run 48; a failure that only surfaces after the restore, i.e. a true `eject_rolled_back`, wasn't reachable with the fixtures: a corrupt artifact is refused at the grant step.)
6063
1p. Re-verify the maintainer-closed fixes with real pip (closed 2026-10-22 to 10-27): #1623, #1602, #1587, #1104, #819. (#740 verified on the eject path on run 48; #1567's mixed-EOL case passes on the eject path; the plain vendored planner over a mixed file is still to re-check.)
6164

6265
1n. #1623 follow-ups: re-verify once fixed (`vendor --revert`, `remove` of the first-vendored package, three packages, CRLF pip-lock file). Also check that comments above restored keys survive (the Poetry handover 20261016T202635Z-from-poetry; root cause noted in #1623), and a hosted → vendored takeover over a two-package `pip lock` file.
@@ -86,6 +89,9 @@ Commands covered on Linux: scan (all modes), get (hosted, agent `-g`), rollback,
8689

8790
## Known non-bugs
8891

92+
- Hosted scan with the pin only in a `-r` include beside a `pip lock` pylock skips with `redirect_unattributable` (pylock wired vs include unpatched) and writes nothing, exit 0. This is the same class as "hosted rewrites only the root requirements.txt"; it's fail-closed (2026-10-28, run 50).
93+
- A pylock whose packages carry files from a non-PyPI index and record no `index` makes the hosted → vendored restore refuse (`redirect_revert_failed`, "restore it from version control"). That's correct fail-closed behaviour, and also a harness trap (2026-10-28).
94+
8995
- A requirements.txt only in a subdirectory (`svc/requirements.txt`, not `-r`-included from the root) is found by neither hosted scan nor the eject (consistent no-op, 2026-10-28).
9096
- Since #1648, plain `vendor` with any hosted pin in scope ejects only the hosted pins and ignores `.socket/manifest.json` (documented in CLI_CONTRACT.md "Takeover reconciliation"); vendoring manifest patches takes `scan --mode vendored`.
9197

0 commit comments

Comments
 (0)