Skip to content

Commit 3013b98

Browse files
committed
fix(socket): a signal during the handshake is not the deadline
wait_before_recv took any wait that ended without a readable socket for the deadline. Without a stop token that wait is one poll, and a signal ends poll with EINTR even when its handler was installed with SA_RESTART, which restarts the blocking recv the wait stands in front of. A host with such a handler (a profiler's SIGPROF, a timer) saw handshakes fail as "TLS handshake timed out" long before the time was up; on master the recv restarted and the handshake went on. A wait that ends early is now waited again for the time that is left, and the deadline is hit only when the clock has passed it. The new case signals the handshaking thread every 100 ms under a 1000 ms timeout; before this change it ended after 100 ms. The move operations carry deadline_hit_ with deadline_.
1 parent cfdab98 commit 3013b98

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

‎src/socket.cppm‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public:
5252

5353
// Move constructor
5454
Socket(Socket&& other) noexcept
55-
: fd_(other.fd_), stop_(std::move(other.stop_)), deadline_(other.deadline_) {
55+
: fd_(other.fd_), stop_(std::move(other.stop_)), deadline_(other.deadline_)
56+
, deadline_hit_(other.deadline_hit_) {
5657
other.fd_ = INVALID_SOCKET_FD;
5758
}
5859

@@ -63,6 +64,7 @@ public:
6364
fd_ = other.fd_;
6465
stop_ = std::move(other.stop_);
6566
deadline_ = other.deadline_;
67+
deadline_hit_ = other.deadline_hit_;
6668
other.fd_ = INVALID_SOCKET_FD;
6769
}
6870
return *this;
@@ -91,17 +93,27 @@ public:
9193
// What `bio_recv` calls ahead of a `recv` that would otherwise block. True
9294
// when a `recv` now will not block. With neither a token nor a deadline it
9395
// answers true without waiting, so the plain `recv` is as it was.
96+
//
97+
// A wait that ends before the deadline is waited again for what is left: a
98+
// signal ends `poll` with EINTR even under SA_RESTART, which the blocking
99+
// `recv` this wait stands in front of would have restarted. The deadline is
100+
// hit only when the clock says so.
94101
bool wait_before_recv() {
95102
if (!stop_.stop_possible() && !deadline_) return true;
96-
int wait = -1;
97-
if (deadline_) {
98-
const auto left = std::chrono::ceil<std::chrono::milliseconds>(
99-
*deadline_ - std::chrono::steady_clock::now()).count();
100-
wait = left > 0 ? static_cast<int>(left) : 0;
103+
for (;;) {
104+
int wait = -1;
105+
if (deadline_) {
106+
const auto left = std::chrono::ceil<std::chrono::milliseconds>(
107+
*deadline_ - std::chrono::steady_clock::now()).count();
108+
wait = left > 0 ? static_cast<int>(left) : 0;
109+
}
110+
if (wait_readable(wait)) return true;
111+
if (stop_.stop_requested() || !deadline_) return false;
112+
if (std::chrono::steady_clock::now() >= *deadline_) {
113+
deadline_hit_ = true;
114+
return false;
115+
}
101116
}
102-
if (wait_readable(wait)) return true;
103-
if (deadline_ && !stop_.stop_requested()) deadline_hit_ = true;
104-
return false;
105117
}
106118

107119
bool connect(const char* host, int port, int timeoutMs) {

‎tests/test_handshake_timeout.cpp‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import std;
1515
#include <gtest/gtest.h>
1616
#include "proxy_test_server.hpp"
1717
#include "tls_test_server.hpp"
18+
#ifndef _WIN32
19+
#include <pthread.h>
20+
#include <signal.h>
21+
#endif
1822

1923
import mcpplibs.tinyhttps;
2024
#ifndef _LIBCPP_VERSION
@@ -215,6 +219,50 @@ TEST_F(HandshakeTimeoutTest, ABytePerReadDoesNotHoldTheHandshakeOpen) {
215219
EXPECT_LT(since_ms(start), kPromptMs);
216220
}
217221

222+
// A signal whose handler was installed with SA_RESTART restarts a blocking
223+
// `recv`, so it never ended a handshake before the deadline existed. The wait
224+
// that the deadline puts ahead of each `recv` is a `poll`, which a signal ends
225+
// with EINTR whatever the flag; that has to be waited out, not taken for the
226+
// deadline. The signals go to this thread, the one in the handshake.
227+
#ifndef _WIN32
228+
namespace {
229+
void ignore_signal(int) {}
230+
}
231+
232+
TEST_F(HandshakeTimeoutTest, ASignalDuringTheHandshakeIsNotATimeout) {
233+
Silent peer;
234+
ASSERT_FALSE(peer.failed());
235+
236+
struct sigaction action {}, previous {};
237+
action.sa_handler = ignore_signal;
238+
action.sa_flags = SA_RESTART;
239+
sigemptyset(&action.sa_mask);
240+
ASSERT_EQ(sigaction(SIGUSR1, &action, &previous), 0);
241+
242+
auto cfg = test_config();
243+
cfg.connectTimeoutMs = 1000;
244+
https::HttpClient client(cfg);
245+
const pthread_t self = pthread_self();
246+
std::atomic<bool> done { false };
247+
std::thread signaller([&] {
248+
for (int i = 0; i < 5 && !done.load(); ++i) {
249+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
250+
if (!done.load()) pthread_kill(self, SIGUSR1);
251+
}
252+
});
253+
const auto start = Clock::now();
254+
auto res = client.send(get(peer.url()));
255+
const auto took = since_ms(start);
256+
done = true;
257+
signaller.join();
258+
sigaction(SIGUSR1, &previous, nullptr);
259+
260+
EXPECT_EQ(res.statusText, "TLS handshake timed out");
261+
EXPECT_GE(took, 900) << "the handshake ended at a signal, not at the deadline";
262+
EXPECT_LT(took, kPromptMs);
263+
}
264+
#endif
265+
218266
// connectTimeoutMs is for establishing the connection. Once the handshake is
219267
// done a read waits as long as it always did: here 1600 ms of silence after a
220268
// 1000 ms timeout, and then the bytes arrive. The timeout leaves room for a

0 commit comments

Comments
 (0)