|
| 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. |
0 commit comments