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))