Commit4584098dRecorded4 Aug 2026Repositorysigil-tls
Bump to 0.16.5: bounded TLS handshake + status-reporting connect
Message
Adds the positive control the timeout tests were missing: a peer that answers immediately with non-TLS bytes must come back as handshake-failed, promptly, with the deadline armed. Without it every assertion in the group was a timeout assertion, and a bound that reported handshake-timeout for every failure would have passed all of them.
Release notes drafted in-branch for the tag message per procedures/release-notes.
Changed
RELEASE_NOTES_v0.16.5.md | 45 +++++++++++++++++++++++++++++++++++++++++++++
package.sgl | 2 +-
test/test-tls.sgl | 25 +++++++++++++++++++++++++
3 files changed, 71 insertions(+), 1 deletion(-)Diff
RELEASE_NOTES_v0.16.5.mdadded
@@ -0,0 +1,45 @@
+1
Bound 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.+4
Everything after that, the entire TLS handshake, was an unbounded blocking+5
read. A server that accepts a TCP connection and then never sends a+6
ServerHello left the caller parked in `read()` with no way out. Under Sigil's+7
cooperative scheduler that does not fail one request, it freezes the whole+8
process, including whatever else was sharing the scheduler.+9
+10
This is not a theoretical hazard. It ran a production monitoring service for+11
55 days while every health surface, the process, the service status and the+12
listening port, reported it healthy.+13
+14
`tls-connect` and `tls-upgrade` now take an optional handshake timeout, and+15
two new procedures report why a connection attempt failed.+16
+17
## Changes+18
+19
- `tls-connect hostname port [connect-timeout-ms [handshake-timeout-ms]]`+20
bounds the handshake. The deadline covers the whole handshake rather than+21
each individual read, so a peer sending one byte at a time cannot extend it.+22
- `tls-upgrade socket hostname [handshake-timeout-ms]` gains the same bound.+23
An upgraded socket is already connected, so all of its handshake sits past+24
the connect phase and its unbounded window was strictly wider than+25
`tls-connect`'s.+26
- `tls-connect/status` and `tls-upgrade/status` return+27
`(status . connection-or-#f)`. `"handshake-timeout"` means the peer accepted+28
and then went silent; `"handshake-failed"` means the peer refused us. Those+29
are different facts and callers retry them differently, which a single `#f`+30
cannot express.+31
- Fixed a file descriptor leak on the `tls-upgrade` failure path. The socket+32
object is marked closed before the handshake runs, so nothing else owned+33
that descriptor. It was hard to reach before; a bounded handshake makes+34
timeouts an ordinary outcome and the leak reachable.+35
+36
## Upgrade notes+37
+38
Nothing to do. Both timeouts are opt-in: omitted or non-positive keeps the+39
original blocking behaviour exactly, and an established connection's reads are+40
unchanged either way because the handshake deadline is unwound before the+41
connection is returned. On Windows the handshake timeout is ignored, matching+42
how the connect timeout already behaves there.+43
+44
Callers that want the protection pass a handshake timeout explicitly. Anything+45
long running that makes outbound HTTPS calls should.package.sglmodified
@@ -9,7 +9,7 @@
9
10
(package 11
name: "sigil-tls"−12
version: "0.16.4"+12
version: "0.16.5" 13
sigil: "^0.17" 14
description: "TLS/SSL connections for Sigil" 15
url: "https://codeberg.org/sigil/sigil-tls"test/test-tls.sglmodified
@@ -151,6 +151,31 @@
151
(assert-true (>= dt 0.5)) 152
(assert-true (<= dt 5.0))))) 153
+154
(test "a peer that answers with non-TLS bytes is handshake-failed, not handshake-timeout"+155
;; POSITIVE CONTROL FOR THE BOUND ITSELF. Everything above asserts a+156
;; timeout; a bound that reported "handshake-timeout" for every failure+157
;; would satisfy all of it. Here the peer DOES answer, immediately, with+158
;; bytes that are not a TLS record. With the deadline armed at 2s the+159
;; call must come back fast AND with the other status.+160
(let* ((server (tcp-listen 0))+161
(port (stall-port server))+162
(client (tcp-connect "127.0.0.1" port))+163
(peer (tcp-accept server)))+164
(assert-true (socket? client))+165
(assert-true (socket? peer))+166
;; Queued before the upgrade, so no second task is needed to send it.+167
(socket-write peer "NOT-A-TLS-RECORD\r\n\r\n")+168
(let* ((measured (elapsed-seconds+169
(lambda () (tls-upgrade/status client "localhost" 2000))))+170
(dt (car measured))+171
(r (cdr measured)))+172
(socket-close peer)+173
(socket-close server)+174
(assert-equal "handshake-failed" (car r))+175
(assert-false (cdr r))+176
;; Promptly: it did not sit out the deadline.+177
(assert-true (< dt 1.0)))))+178
179
(test "tls-upgrade/status reports handshake-timeout" 180
(let* ((server (tcp-listen 0)) 181
(port (stall-port server))