---
id: 476e0fbb-25b5-4051-92ae-8e38a1fef02e
title: 'serve: unbounded thread-per-connection + 500ms ack sleep = trivial availability DoS'
state: closed
labels:
- security
assignees: []
milestone: null
created: 2026-10-27T14:53:46.365269127Z
updated: 2026-10-27T15:48:00.976093000Z
---
`hu13 serve` is thread-per-connection with no cap, and every inbox response sleeps 500 ms.

Verified:

```
before: 1 thread
$ python3 open 300 idle sockets sending "GET / HTTP/1.1\r\nHost: x\r\n" (no terminator)
during: 301 threads   VmRSS 11.8 MB
```

Those sockets cost nothing per connection and can be kept alive indefinitely (a byte every < read timeout). ~10-30k cheap sockets exhaust the thread/pid limit or memory, and the server stops accepting.

Amplifiers:
- inbox handler always `sleep(ACK_DELAY_MS)` (500 ms) before answering, on every path including 400/403/429 — a rejected POST holds a thread for half a second
- `settle()` will additionally read up to 256 KiB from a lying `Content-Length` before answering
- no accept-backlog tuning, no per-IP connection limit, no global connection cap

Related, same class but separate resource: #63 (`git upload-pack` with no timeout/cap).

Fix direction:
- cap concurrent connections (e.g. semaphore of 64-256) and answer 503 immediately when exhausted
- keep the fixed ack delay for *semantic* uniformity, but do not let it pin a thread: either keep it (with the cap above) or use a small timer wheel; the cap is the simpler fix
- optionally lower `POST_READ_TIMEOUT_SECS` and require the head within a shorter window than the body

Acceptance:
- [ ] N idle sockets beyond the cap get refused/closed, not thread-per-connection
- [ ] process thread count has a documented upper bound
- [ ] a test/bench opens >cap sockets and asserts the server still answers `/`
