AtlatestRepositorysigil-tls
1
Bound the TLS handshake so a silent peer cannot block a process forever3
`connect-timeout-ms` stops at the moment the peer accepts the connection.4
Everything after that, the entire TLS handshake, was an unbounded blocking5
read. A server that accepts a TCP connection and then never sends a6
ServerHello left the caller parked in `read()` with no way out. Under Sigil's7
cooperative scheduler that does not fail one request, it freezes the whole8
process, including whatever else was sharing the scheduler.10
This is not a theoretical hazard. It ran a production monitoring service for11
55 days while every health surface, the process, the service status and the12
listening port, reported it healthy.14
`tls-connect` and `tls-upgrade` now take an optional handshake timeout, and15
two new procedures report why a connection attempt failed.17
## Changes19
- `tls-connect hostname port [connect-timeout-ms [handshake-timeout-ms]]`20
bounds the handshake. The deadline is a TOTAL over the handshake, enforced21
on each individual read as a clamp on how long that read may wait.23
That distinction carries the whole value of this release, so it is worth24
stating why. A bound applied only per read is defeated by a peer that DRIPS25
rather than one that goes silent: mbedTLS does not return between handshake26
steps and restarts its read timeout on every partial read, so each dribbled27
byte resets the clock. Measured against an implementation that got this28
wrong, a peer sending one byte every 600 ms made a 1000 ms bound take 1329
seconds to fire, and it never fired at all while the drip continued. A30
per-read timeout is still kept underneath as a backstop; the two bound31
different things and neither replaces the other.32
- `tls-upgrade socket hostname [handshake-timeout-ms]` gains the same bound.33
An upgraded socket is already connected, so all of its handshake sits past34
the connect phase and its unbounded window was strictly wider than35
`tls-connect`'s.36
- `tls-connect/status` and `tls-upgrade/status` return37
`(status . connection-or-#f)`. `"handshake-timeout"` means the peer accepted38
and then went silent; `"handshake-failed"` means the peer refused us. Those39
are different facts and callers retry them differently, which a single `#f`40
cannot express.41
- `tls-write` on a non-blocking connection returns the bytes it actually42
committed, possibly zero, instead of spinning. It previously retried43
`WANT_WRITE` in a hot loop that never returned and never yielded, so a peer44
that stopped reading could pin a CPU indefinitely and no caller could bound45
a write at all. Blocking connections are unchanged. `tls-write` also gains46
optional start and end byte offsets, mirroring `socket-write`, so a write47
loop resending the tail of a large body stays O(n) rather than O(n squared).49
## Fixed51
- A file descriptor leak on the `tls-upgrade` failure path. The socket object52
is marked closed before the handshake runs, so nothing else owned that53
descriptor and nothing would ever close it.55
This one matters more than its size suggests. The leak was hard to reach56
while handshake failures were rare, and bounding the handshake makes57
timeouts an ordinary outcome. Shipping the bound without this would have58
traded a hang for descriptor exhaustion weeks later, by which time the59
connection between the two would have been invisible.61
## Upgrade notes63
Nothing to do. Both timeouts are opt-in: omitted or non-positive keeps the64
original blocking behaviour exactly, and an established connection's reads are65
unchanged either way because the handshake deadline is unwound before the66
connection is returned. On Windows the handshake timeout is ignored, matching67
how the connect timeout already behaves there.69
Callers that want the protection pass a handshake timeout explicitly. Anything70
long running that makes outbound HTTPS calls should.