Skip to main content

max / makenotwork

Cap git upload-pack child lifetime to bound permit starvation A client that holds the connection open without reading makes git upload-pack block on a full stdout pipe and never exit, pinning its concurrency permit indefinitely; enough slow clients starve all clones at the handshake. Kill the child past GIT_SMART_HTTP_TIMEOUT_SECS (300s, generous for a large repo on a slow link) so the permit is always freed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-15 23:22 UTC
Commit: a04dbf2b8f5342e5c21f30e87bb87157a2be1f73
Parent: 8324228
2 files changed, +16 insertions, -2 deletions
@@ -250,6 +250,12 @@ pub const GIT_UPLOAD_PACK_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB upload-p
250 250 // upload-pack` child and streams a packfile; this bounds the process fan-out so
251 251 // a burst of clones on a large repo can't exhaust processes/memory.
252 252 pub const GIT_SMART_HTTP_MAX_CONCURRENT: usize = 8;
253 + // Hard ceiling on how long a single clone's `git upload-pack` child may run
254 + // while holding its concurrency permit. A client that stops reading makes git
255 + // block on a full stdout pipe and never exit, pinning the permit; killing it
256 + // past this deadline keeps slow/stuck clones from starving the budget. Generous
257 + // enough for a large repo over a slow link.
258 + pub const GIT_SMART_HTTP_TIMEOUT_SECS: u64 = 300;
253 259
254 260 // -- Webhook security --
255 261 pub const WEBHOOK_TIMESTAMP_TOLERANCE_SECS: u64 = 300; // 5 minutes
@@ -209,10 +209,18 @@ pub(super) async fn smart_http_upload_pack(
209 209
210 210 // Reap the child and release the permit once it exits. If the client
211 211 // disconnects, the response body (and `stdout`) drops, git gets SIGPIPE and
212 - // exits, so `wait()` returns and the permit is freed — no leak either way.
212 + // exits, so `wait()` returns and the permit is freed. A client that holds
213 + // the connection open without reading would otherwise make git block on a
214 + // full stdout pipe and never exit, pinning the permit indefinitely — so cap
215 + // the wait and kill the child past a generous deadline, bounding the
216 + // slow-client permit-starvation window (Run #21 Performance).
213 217 tokio::spawn(async move {
214 218 let _permit = permit;
215 - let _ = child.wait().await;
219 + let deadline = std::time::Duration::from_secs(constants::GIT_SMART_HTTP_TIMEOUT_SECS);
220 + if tokio::time::timeout(deadline, child.wait()).await.is_err() {
221 + let _ = child.start_kill();
222 + let _ = child.wait().await;
223 + }
216 224 });
217 225
218 226 Response::builder()