AtlatestRepositorysigil-tls

sigil-tls / treeRELEASE_NOTES_v0.16.5.md

Read it rendered

1Bound the TLS handshake so a silent peer cannot block a process forever
2
3`connect-timeout-ms` stops at the moment the peer accepts the connection.
4Everything after that, the entire TLS handshake, was an unbounded blocking
5read. A server that accepts a TCP connection and then never sends a
6ServerHello left the caller parked in `read()` with no way out. Under Sigil's
7cooperative scheduler that does not fail one request, it freezes the whole
8process, including whatever else was sharing the scheduler.
9
10This is not a theoretical hazard. It ran a production monitoring service for
1155 days while every health surface, the process, the service status and the
12listening port, reported it healthy.
14`tls-connect` and `tls-upgrade` now take an optional handshake timeout, and
15two new procedures report why a connection attempt failed.
17## Changes
19- `tls-connect hostname port [connect-timeout-ms [handshake-timeout-ms]]`
20 bounds the handshake. The deadline is a TOTAL over the handshake, enforced
21 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 worth
24 stating why. A bound applied only per read is defeated by a peer that DRIPS
25 rather than one that goes silent: mbedTLS does not return between handshake
26 steps and restarts its read timeout on every partial read, so each dribbled
27 byte resets the clock. Measured against an implementation that got this
28 wrong, a peer sending one byte every 600 ms made a 1000 ms bound take 13
29 seconds to fire, and it never fired at all while the drip continued. A
30 per-read timeout is still kept underneath as a backstop; the two bound
31 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 past
34 the connect phase and its unbounded window was strictly wider than
35 `tls-connect`'s.
36- `tls-connect/status` and `tls-upgrade/status` return
37 `(status . connection-or-#f)`. `"handshake-timeout"` means the peer accepted
38 and then went silent; `"handshake-failed"` means the peer refused us. Those
39 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 actually
42 committed, possibly zero, instead of spinning. It previously retried
43 `WANT_WRITE` in a hot loop that never returned and never yielded, so a peer
44 that stopped reading could pin a CPU indefinitely and no caller could bound
45 a write at all. Blocking connections are unchanged. `tls-write` also gains
46 optional start and end byte offsets, mirroring `socket-write`, so a write
47 loop resending the tail of a large body stays O(n) rather than O(n squared).
49## Fixed
51- A file descriptor leak on the `tls-upgrade` failure path. The socket object
52 is marked closed before the handshake runs, so nothing else owned that
53 descriptor and nothing would ever close it.
55 This one matters more than its size suggests. The leak was hard to reach
56 while handshake failures were rare, and bounding the handshake makes
57 timeouts an ordinary outcome. Shipping the bound without this would have
58 traded a hang for descriptor exhaustion weeks later, by which time the
59 connection between the two would have been invisible.
61## Upgrade notes
63Nothing to do. Both timeouts are opt-in: omitted or non-positive keeps the
64original blocking behaviour exactly, and an established connection's reads are
65unchanged either way because the handshake deadline is unwound before the
66connection is returned. On Windows the handshake timeout is ignored, matching
67how the connect timeout already behaves there.
69Callers that want the protection pass a handshake timeout explicitly. Anything
70long running that makes outbound HTTPS calls should.