Skip to content

Commit be6ebd5

Browse files
committed
bughunt(bun): probe isolated linker + bun patch
1 parent f6b7fb9 commit be6ebd5

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

‎.github/workflows/bughunt-bun.yml‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
name: bughunt-bun probe
2+
on:
3+
push:
4+
branches: ['bughunt/bun/**']
5+
permissions:
6+
contents: read
7+
jobs:
8+
probe:
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
os: [ubuntu-latest, macos-latest, windows-latest]
13+
runs-on: ${{ matrix.os }}
14+
timeout-minutes: 45
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
18+
with:
19+
persist-credentials: false
20+
- name: Cache cargo
21+
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
22+
with:
23+
key: bun-native
24+
save-if: false
25+
- name: Build CLI
26+
run: cargo build --locked -p socket-patch-cli
27+
- name: Probe
28+
shell: bash
29+
run: |
30+
mkdir -p "$RUNNER_TEMP/bh"
31+
cat > "$RUNNER_TEMP/bh/driver.py" <<'PYEOF'
32+
import sys, os, json, io, tarfile, gzip, hashlib, base64, urllib.request, re, threading, shutil, subprocess, tempfile, urllib.parse
33+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
34+
SP = os.path.abspath(sys.argv[1]); BUN = shutil.which("bun") if len(sys.argv) < 3 else sys.argv[2]
35+
ROOT = tempfile.mkdtemp(prefix="bh-")
36+
MARK = b"/* SOCKET-PATCHED */\n"; TOKEN = "22222222-2222-4222-8222-222222222222"
37+
def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
38+
SPECS = []
39+
def mkpatch(name, ver, uuid):
40+
raw = urllib.request.urlopen(f"https://registry.npmjs.org/{name}/-/{name}-{ver}.tgz").read()
41+
tin = tarfile.open(fileobj=io.BytesIO(raw), mode="r:gz"); files = {}
42+
for m in tin.getmembers():
43+
if m.isfile(): files[m.name] = tin.extractfile(m).read()
44+
top = next(iter(files)).split('/')[0]; target = f"{top}/index.js"
45+
before = files[target]; after = MARK + before
46+
buf = io.BytesIO(); tout = tarfile.open(fileobj=buf, mode="w", format=tarfile.USTAR_FORMAT)
47+
for rel, data in files.items():
48+
d = after if rel == target else data
49+
ti = tarfile.TarInfo(rel); ti.mode = 0o644; ti.mtime = 0; ti.size = len(d); tout.addfile(ti, io.BytesIO(d))
50+
tout.close(); gz = io.BytesIO()
51+
with gzip.GzipFile(fileobj=gz, mode="wb", mtime=0) as g: g.write(buf.getvalue())
52+
tgz = gz.getvalue()
53+
SPECS.append({"uuid": uuid, "purl": f"pkg:npm/{name}@{ver}", "name": name, "version": ver, "leaf": f"{name}-{ver}.tgz", "tgz": tgz,
54+
"sri": "sha512-" + base64.b64encode(hashlib.sha512(tgz).digest()).decode(),
55+
"files": {"package/index.js": {"beforeHash": gsha(before), "afterHash": gsha(after), "blobContent": base64.b64encode(after).decode()}}})
56+
PORT = [0]
57+
class H(BaseHTTPRequestHandler):
58+
def log_message(self, *a): pass
59+
def j(self, o, code=200):
60+
b = json.dumps(o).encode(); self.send_response(code); self.send_header("content-type", "application/json"); self.send_header("content-length", str(len(b))); self.end_headers(); self.wfile.write(b)
61+
def hosted(self, s): return f"http://127.0.0.1:{PORT[0]}/patch/npm/{s['name']}/{s['version']}/{TOKEN}/{s['uuid']}/{s['leaf']}"
62+
def pinfo(self, s): return {"uuid": s["uuid"], "purl": s["purl"], "tier": "free", "cveIds": ["CVE-2026-0001"], "ghsaIds": ["GHSA-bunh-unt0-0001"], "severity": "high", "title": "bughunt fixture"}
63+
def do_POST(self):
64+
p = urllib.parse.urlparse(self.path).path; n = int(self.headers.get("content-length") or 0); b = json.loads(self.rfile.read(n) or b"{}")
65+
if p.endswith("/patches/batch"):
66+
want = {c.get("purl") for c in b.get("components", [])}
67+
return self.j({"packages": [{"purl": s["purl"], "patches": [self.pinfo(s)]} for s in SPECS if s["purl"] in want], "canAccessPaidPatches": False})
68+
if p.endswith("/patches/package"):
69+
return self.j({"results": {s["uuid"]: {"status": "granted", "url": self.hosted(s), "purl": s["purl"], "artifacts": [{"kind": "tarball", "url": self.hosted(s), "integrity": {"sha512": s["sri"]}}], "registryOverride": None} for s in SPECS}})
70+
self.j({}, 404)
71+
def do_GET(self):
72+
p = urllib.parse.unquote(urllib.parse.urlparse(self.path).path)
73+
m = re.search(r"/patches/view/([0-9a-f-]+)$", p)
74+
if m:
75+
for s in SPECS:
76+
if s["uuid"] == m.group(1):
77+
return self.j({"uuid": s["uuid"], "purl": s["purl"], "publishedAt": "2026-01-01T00:00:00Z", "files": s["files"],
78+
"vulnerabilities": {"GHSA-bunh-unt0-0001": {"cves": ["CVE-2026-0001"], "summary": "s", "severity": "high", "description": "d"}},
79+
"description": "x", "license": "MIT", "tier": "free"})
80+
if "/patches/by-package/" in p:
81+
pu = p.split("/patches/by-package/", 1)[1]
82+
return self.j({"patches": [dict(self.pinfo(s), publishedAt="2026-01-01T00:00:00Z", description="x", license="MIT", vulnerabilities={}) for s in SPECS if s["purl"] == pu], "canAccessPaidPatches": False})
83+
m = re.match(r"/patch/npm/.+/([0-9a-f-]+)/[^/]+$", p)
84+
if m:
85+
for s in SPECS:
86+
if s["uuid"] == m.group(1):
87+
self.send_response(200); self.send_header("content-length", str(len(s["tgz"]))); self.end_headers(); self.wfile.write(s["tgz"]); return
88+
self.j({}, 404)
89+
def run(cmd, cwd, env=None):
90+
e = dict(os.environ); e.update(env or {}); e["SOCKET_TELEMETRY_DISABLED"] = "1"
91+
r = subprocess.run(cmd, cwd=cwd, env=e, capture_output=True, text=True, encoding="utf-8", errors="replace")
92+
return r.returncode, r.stdout, r.stderr
93+
def sp(args, cwd):
94+
return run([SP] + args + ["--api-url", f"http://127.0.0.1:{PORT[0]}", "--org", "test-org", "--api-token", "fake"], cwd)
95+
def bun(args, cwd): return run([BUN] + args, cwd, {"BUN_INSTALL_CACHE_DIR": os.path.join(cwd, ".bun-cache")})
96+
def marker(path):
97+
try: return open(path, "rb").read().startswith(MARK)
98+
except Exception as ex: return f"missing:{ex.__class__.__name__}"
99+
def env_of(out):
100+
try: return json.loads(out)
101+
except Exception: return {"_raw": out[:500]}
102+
RESULTS = []
103+
def case_isolated(tag, files):
104+
d = os.path.join(ROOT, tag); os.makedirs(d)
105+
for rel, content in files.items():
106+
os.makedirs(os.path.dirname(os.path.join(d, rel)) or d, exist_ok=True); open(os.path.join(d, rel), "w").write(content)
107+
rc, o, e = bun(["install"], d)
108+
store = os.path.join(d, "node_modules", ".bun")
109+
layout = sorted(os.listdir(store)) if os.path.isdir(store) else "no .bun store (hoisted)"
110+
rc2, o2, e2 = sp(["scan", "--mode", "agent", "--json", "--yes"], d); env = env_of(o2)
111+
acts = [(p.get("purl"), p.get("action"), p.get("errorCode")) for p in env.get("apply", {}).get("patches", [])]
112+
mm = os.path.join(d, "node_modules", ".bun", "minimist@1.2.8", "node_modules", "minimist", "index.js")
113+
if not os.path.exists(mm): mm = os.path.join(d, "node_modules", "minimist", "index.js")
114+
RESULTS.append({"case": tag, "scan_stderr": e2[-400:], "bun_install_rc": rc, "layout": layout, "scan_rc": rc2, "status": env.get("status"),
115+
"lockfileOnly": env.get("lockfileOnlyPackages"), "apply": acts, "minimist_patched_on_disk": marker(mm)})
116+
def case_bunpatch(mode):
117+
d = os.path.join(ROOT, "bunpatch-" + mode); os.makedirs(d)
118+
open(os.path.join(d, "package.json"), "w").write('{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}\n')
119+
bun(["install"], d); bun(["patch", "left-pad"], d)
120+
open(os.path.join(d, "node_modules", "left-pad", "index.js"), "a").write("// USER-BUN-PATCH\n")
121+
rcp, op, ep = bun(["patch", "--commit", "node_modules/left-pad"], d)
122+
has_patch = os.path.isdir(os.path.join(d, "patches"))
123+
rc2, o2, e2 = sp(["scan", "--mode", mode, "--json", "--yes"], d); env = env_of(o2)
124+
warns = env.get("redirect", {}).get("warnings") if mode == "hosted" else [x.get("errorCode") for x in env.get("vendor", {}).get("events", []) if x.get("errorCode")]
125+
f = os.path.join(ROOT, "bunpatch-" + mode + "-fresh"); os.makedirs(f)
126+
for n in ["package.json", "bun.lock", "bun.lockb", "patches", ".socket"]:
127+
src = os.path.join(d, n)
128+
if os.path.isdir(src): shutil.copytree(src, os.path.join(f, n))
129+
elif os.path.exists(src): shutil.copy2(src, os.path.join(f, n))
130+
rc3, o3, e3 = bun(["install", "--frozen-lockfile"], f)
131+
idx = os.path.join(f, "node_modules", "left-pad", "index.js")
132+
try: body = open(idx, "rb").read()
133+
except Exception: body = b""
134+
RESULTS.append({"case": "bunpatch-" + mode, "bun_patch_committed": has_patch and rcp == 0, "scan_rc": rc2, "status": env.get("status"), "warnings": warns,
135+
"fresh_frozen_rc": rc3, "socket_patch_present": body.startswith(MARK), "user_bun_patch_present": b"USER-BUN-PATCH" in body,
136+
"frozen_stderr_tail": e3[-300:] if rc3 else ""})
137+
if __name__ == "__main__":
138+
mkpatch("left-pad", "1.3.0", "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"); mkpatch("minimist", "1.2.8", "11111111-2222-4333-8444-555555555555")
139+
srv = ThreadingHTTPServer(("127.0.0.1", 0), H); PORT[0] = srv.server_address[1]; threading.Thread(target=srv.serve_forever, daemon=True).start()
140+
print("bun", run([BUN, "--version"], ROOT)[1].strip(), "platform", sys.platform)
141+
case_isolated("isolated-bunfig", {"package.json": '{"name":"app","version":"1.0.0","dependencies":{"mkdirp":"0.5.6","left-pad":"1.3.0"}}\n', "bunfig.toml": '[install]\nlinker = "isolated"\n'})
142+
case_isolated("workspace-default-linker", {"package.json": '{"name":"root","private":true,"workspaces":["packages/*"]}\n', "packages/a/package.json": '{"name":"a","version":"1.0.0","dependencies":{"mkdirp":"0.5.6"}}\n'})
143+
case_isolated("hoisted-control", {"package.json": '{"name":"app","version":"1.0.0","dependencies":{"mkdirp":"0.5.6","left-pad":"1.3.0"}}\n', "bunfig.toml": '[install]\nlinker = "hoisted"\n'})
144+
for m in ("hosted", "vendored"): case_bunpatch(m)
145+
print("RESULTS-JSON " + json.dumps(RESULTS))
146+
for r in RESULTS: print(json.dumps(r))
147+
PYEOF
148+
cat > "$RUNNER_TEMP/bh/fetchbun.py" <<'PYEOF'
149+
import sys, os, urllib.request, zipfile, io, platform
150+
v, dest = sys.argv[1], sys.argv[2]
151+
osn = {"linux": "linux", "darwin": "darwin", "win32": "windows"}[sys.platform]
152+
arch = "aarch64" if platform.machine().lower() in ("arm64", "aarch64") else "x64"
153+
name = f"bun-{osn}-{arch}"
154+
for i in range(4):
155+
try:
156+
data = urllib.request.urlopen(f"https://github.com/oven-sh/bun/releases/download/bun-v{v}/{name}.zip").read(); break
157+
except Exception as e:
158+
print("retry", e)
159+
zipfile.ZipFile(io.BytesIO(data)).extractall(dest)
160+
exe = os.path.join(dest, name, "bun.exe" if osn == "windows" else "bun")
161+
os.chmod(exe, 0o755); print(exe)
162+
PYEOF
163+
SP="$PWD/target/debug/socket-patch"; PY=python3; if [ "$RUNNER_OS" = "Windows" ]; then SP="$SP.exe"; PY=python; fi
164+
for v in 1.2.23 1.3.14 1.4.2; do
165+
echo "::group::bun $v"
166+
BUNEXE=$("$PY" "$RUNNER_TEMP/bh/fetchbun.py" "$v" "$RUNNER_TEMP/bun-$v" | tail -1)
167+
"$PY" "$RUNNER_TEMP/bh/driver.py" "$SP" "$BUNEXE" 2>&1 | tee "$RUNNER_TEMP/bh/out-$v.txt" || true
168+
echo "::endgroup::"
169+
done
170+
echo "===== SUMMARY ====="
171+
for v in 1.2.23 1.3.14 1.4.2; do echo "--- bun $v (${{ matrix.os }})"; grep '^{' "$RUNNER_TEMP/bh/out-$v.txt" | sed 's/"scan_stderr": "[^"]*", //' || true; done

0 commit comments

Comments
 (0)