Skip to content

Commit c5ce895

Browse files
quaesitor-scientiamclaudewozcode
authored
net.http: fix TLS server read/handshake timeouts and idle-shutdown fd race (vlang#27434)
* net.http: bound the TLS server handshake timeout for infinite accept_timeout The TLS server handshake runs on the accept thread, and its timeout was derived directly from `accept_timeout` (`handshake_timeout := accept_timeout`). With `accept_timeout <= 0` (block indefinitely waiting to accept), the handshake timeout therefore became infinite too, so a client that completes the TCP connect and then stalls mid-TLS-handshake wedged the accept loop forever: no new connections were accepted, and `stop()` was never observed. That is the hang class vlang#27429 set out to remove, still reachable for the `accept_timeout <= 0` configuration. Fall back to a finite `tls_handshake_timeout` when `accept_timeout <= 0`. Note: this reverses the deliberate behavior added in "preserve zero TLS handshake timeout"; the corresponding test is updated. Flagging for @medvednikov per the discussion on vlang#27433. (Item #2 from vlang#27433 — read_timeout ignored on HTTPS — was fixed independently on master by "fix master ci failures", so it is not included here. The close_idle fd-reuse race, item #4, is left for a separate change now that master added an out-of-lock net.close on Windows.) Refs vlang#27433. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * net.http: make TLS handshake fallback timeout configurable per Server `tls_handshake_timeout` was a module-level constant with no Server-field override, inconsistent with `read_timeout`, `write_timeout`, and `accept_timeout` which are all `pub mut` fields. Add `Server.tls_handshake_timeout` (default 30 s) and thread it through `tls_accept_timeouts` as a parameter so users who need a tighter budget (hardened public-facing server) or a looser one (embedded devices with slow crypto hardware) can set it directly. Also fix the misleading doc comment: the fallback fires only when `accept_timeout` is explicitly zero or `net.infinite_timeout`, not whenever the user "did not set a finite accept_timeout" (the default is already finite at 30 s). Co-Authored-By: WOZCODE <contact@withwoz.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: WOZCODE <contact@withwoz.com>
1 parent 4a6645f commit c5ce895

3 files changed

Lines changed: 45 additions & 9 deletions

File tree

vlib/net/http/server.v

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub mut:
3737
read_timeout time.Duration = 30 * time.second
3838
write_timeout time.Duration = 30 * time.second
3939
accept_timeout time.Duration = 30 * time.second
40+
tls_handshake_timeout time.Duration = 30 * time.second // fallback handshake budget used when accept_timeout is zero or net.infinite_timeout; ignored on non-TLS servers
4041
pool_channel_slots int = 1024
4142
worker_num int = runtime.nr_jobs()
4243
max_keep_alive_requests int = 100 // max requests per keep-alive connection (0 = unlimited)

vlib/net/http/server_tls_notd_use_openssl.v

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,23 @@ import net.mbedtls
1010

1111
const tls_accept_poll_timeout = 100 * time.millisecond
1212

13-
fn tls_accept_timeouts(accept_timeout time.Duration) (time.Duration, time.Duration) {
14-
handshake_timeout := accept_timeout
15-
accept_poll_timeout := if accept_timeout > 0 && accept_timeout < tls_accept_poll_timeout {
13+
// tls_handshake_timeout is the default value for Server.tls_handshake_timeout,
14+
// used as the fallback handshake budget when Server.accept_timeout is zero or
15+
// net.infinite_timeout. The handshake runs on the accept thread, so without a
16+
// finite bound a client that completes the TCP connect and then stalls
17+
// mid-handshake would wedge the accept loop forever.
18+
const tls_handshake_timeout = 30 * time.second
19+
20+
fn tls_accept_timeouts(accept_timeout time.Duration, handshake_fallback time.Duration) (time.Duration, time.Duration) {
21+
// A finite `accept_timeout` doubles as the handshake budget; when it is
22+
// zero or net.infinite_timeout (i64.max), fall back to `handshake_fallback`
23+
// so the handshake still times out and shutdown stays responsive.
24+
// net.infinite_timeout is positive (i64.max), so the > 0 check alone is
25+
// not enough — mbedtls's ssl_timeout_deadline treats it as an infinite
26+
// deadline exactly like 0 or negative values.
27+
is_finite := accept_timeout > 0 && accept_timeout != net.infinite_timeout
28+
handshake_timeout := if is_finite { accept_timeout } else { handshake_fallback }
29+
accept_poll_timeout := if is_finite && accept_timeout < tls_accept_poll_timeout {
1630
accept_timeout
1731
} else {
1832
tls_accept_poll_timeout
@@ -80,7 +94,8 @@ fn (mut s Server) listen_and_serve_tls() {
8094
if s.on_running != unsafe { nil } {
8195
s.on_running(mut s)
8296
}
83-
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(s.accept_timeout)
97+
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(s.accept_timeout,
98+
s.tls_handshake_timeout)
8499
for s.state == .running {
85100
mut conn := listener.accept_with_timeouts(accept_poll_timeout, handshake_timeout) or {
86101
if s.state != .running {
Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
module http
22

3+
import net
34
import time
45

5-
fn test_tls_accept_timeouts_preserve_zero_handshake_timeout() {
6-
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(0)
6+
fn test_tls_accept_timeouts_finite_handshake_for_infinite_accept() {
7+
// An infinite accept timeout (<= 0, or net.infinite_timeout) must still
8+
// yield a finite handshake timeout, so a client stalling mid-handshake
9+
// cannot wedge the accept thread or block shutdown.
10+
// net.infinite_timeout is i64.max (positive), so the > 0 guard alone is
11+
// insufficient — mbedtls treats it as an infinite deadline.
12+
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(0, tls_handshake_timeout)
713
assert accept_poll_timeout == tls_accept_poll_timeout
8-
assert handshake_timeout == 0
14+
assert handshake_timeout == tls_handshake_timeout
15+
_, neg_handshake_timeout := tls_accept_timeouts(-1, tls_handshake_timeout)
16+
assert neg_handshake_timeout == tls_handshake_timeout
17+
_, inf_handshake_timeout := tls_accept_timeouts(net.infinite_timeout, tls_handshake_timeout)
18+
assert inf_handshake_timeout == tls_handshake_timeout
919
}
1020

1121
fn test_tls_accept_timeouts_cap_poll_without_changing_handshake_timeout() {
12-
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(time.second)
22+
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(time.second,
23+
tls_handshake_timeout)
1324
assert accept_poll_timeout == tls_accept_poll_timeout
1425
assert handshake_timeout == time.second
1526
}
1627

1728
fn test_tls_accept_timeouts_keep_short_accept_timeout() {
18-
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(50 * time.millisecond)
29+
accept_poll_timeout, handshake_timeout := tls_accept_timeouts(50 * time.millisecond,
30+
tls_handshake_timeout)
1931
assert accept_poll_timeout == 50 * time.millisecond
2032
assert handshake_timeout == 50 * time.millisecond
2133
}
34+
35+
fn test_tls_accept_timeouts_configurable_fallback() {
36+
// When accept_timeout is infinite, the handshake_fallback parameter
37+
// (Server.tls_handshake_timeout) governs the budget — not the constant.
38+
custom_fallback := 5 * time.second
39+
_, handshake_timeout := tls_accept_timeouts(0, custom_fallback)
40+
assert handshake_timeout == custom_fallback
41+
}

0 commit comments

Comments
 (0)