id: ee5121b8-e3da-4667-88b7-243e11ec27f6 title: 'serve: git upload-pack has no timeout, no concurrency cap, and fully buffers the pack' state: closed labels:
- security assignees: [] milestone: null created: 2026-10-27T14:53:54.653824678Z updated: 2026-10-27T15:48:01.072598268Z
/repo.bundle/git-upload-pack spawns git upload-pack per POST with no timeout, no concurrency limit, and buffers the entire pack in RAM (wait_with_output() → Vec<u8> → respond).
Problems:
- No timeout: nothing bounds the child. The 5 s socket read timeout applies to our reads from the client, not to git finishing; a client that dribbles its want-list or stalls mid-pack keeps a thread + a
gitprocess alive for as long as it likes. The 1 MiB request cap only bounds the request. - No concurrency cap: N parallel clones = N
git upload-packprocesses + N full packs in memory. Two or three concurrent clones of a large repo can OOM the container (default Docker memory limits are generous, Cloudflare-free-boxes are not). - Whole-response buffering:
respond()takes&[u8], so the pack is fully materialized before the first byte is written — no streaming, noContent-Length-free chunked path available in the currentrespond. - Client disconnects are not detected until wait() returns.
Note this is the smart path and is serve-only (dumb /repo.git is static and unaffected), so a deployment that never advertises /repo.bundle still exposes it — the route exists whenever www/repo.bundle exists, which build always writes.
Fix direction:
- spawn
upload-pack, stream stdout to the socket while writing the pack (droprespond's buffering for this route), kill the child on write error - enforce a wall-clock timeout (e.g. 60 s) and a concurrency semaphore; 503 when full
- consider gating the route behind a flag (it is a convenience, not the canonical clone path since #54)
Acceptance:
- a stalled client cannot keep a git process alive past the timeout
- concurrent clones are capped and the cap is documented
- memory use does not scale with pack size for a single clone