Skip to content

Commit 713cc6d

Browse files
Ankit Kumarmeta-codesync[bot]
authored andcommitted
Bound the StopTLS V1 downgrade with a timeout
Summary: Bounds the StopTLS V1 downgrade with the connection's handshake-timeout budget, so a peer that stalls it is dropped rather than holding its transport open indefinitely. Reviewed By: robertroeser Differential Revision: D116715818 fbshipit-source-id: 479375138f8296a71ad768a9550708b03661be5e
1 parent 8d07e6e commit 713cc6d

4 files changed

Lines changed: 104 additions & 9 deletions

File tree

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/connection/security/handler/StopTLSV1Handler.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#pragma once
1818

1919
#include <cstdint>
20+
#include <optional>
2021
#include <utility>
2122

2223
#include <fizz/server/AsyncFizzServer.h>
@@ -50,9 +51,9 @@ namespace apache::thrift::fast_thrift::connection::security::handler {
5051
* StopTLS failures are logged at DBG3 and the connection is dropped.
5152
*
5253
* The downgrades parked here are capped: past the cap a connection is refused
53-
* rather than started. The exchange has no timeout of its own, so a peer that
54-
* negotiates StopTLS V1 and then stalls the downgrade holds its socket until
55-
* it disconnects.
54+
* rather than started. Each one is also bounded by the handshake-timeout
55+
* budget off the incoming message, so a peer that negotiates StopTLS V1 and
56+
* then stalls the downgrade is dropped rather than parked indefinitely.
5657
*/
5758
class StopTLSV1Handler {
5859
public:
@@ -118,9 +119,13 @@ class StopTLSV1Handler {
118119
fizz::server::AsyncFizzServer::UniquePtr fizzServer(fizzPtr);
119120
auto clientAddr = std::move(incoming.clientAddr);
120121
auto extension = std::move(incoming.extension);
122+
auto stopTLSTimeout = incoming.tlsParams
123+
? incoming.tlsParams->handshakeTimeout
124+
: std::nullopt;
121125

122126
util::StopTLSHelper::UniquePtr helper(new util::StopTLSHelper(
123127
std::move(fizzServer),
128+
stopTLSTimeout,
124129
[this](util::StopTLSHelper* h) noexcept { inFlight_.erase(h); },
125130
[this, clientAddr, extension](
126131
folly::AsyncTransport::UniquePtr plaintext,

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/connection/security/util/StopTLSHelper.cpp

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,31 @@
2424

2525
namespace apache::thrift::fast_thrift::connection::security::util {
2626

27+
namespace {
28+
29+
// Zero already means "unbounded" to AsyncStopTLS, so a non-positive budget
30+
// cannot be honoured as a deadline. Degrade to unbounded — the documented
31+
// behavior for an absent budget — and shout, since the only way to arrive
32+
// here is misuse.
33+
std::chrono::milliseconds resolveTimeout(
34+
std::optional<std::chrono::milliseconds> timeout) {
35+
if (timeout && timeout->count() <= 0) {
36+
XLOG(DFATAL) << "StopTLSHelper: pass std::nullopt for unbounded, not "
37+
<< timeout->count() << "ms; treating as unbounded.";
38+
return std::chrono::milliseconds{0};
39+
}
40+
return timeout.value_or(std::chrono::milliseconds{0});
41+
}
42+
43+
} // namespace
44+
2745
StopTLSHelper::StopTLSHelper(
2846
fizz::server::AsyncFizzServer::UniquePtr fizzServer,
47+
std::optional<std::chrono::milliseconds> timeout,
2948
OnTerminal onTerminal,
3049
Callback callback)
3150
: fizzServer_(std::move(fizzServer)),
51+
timeout_(resolveTimeout(timeout)),
3252
onTerminal_(std::move(onTerminal)),
3353
callback_(std::move(callback)) {}
3454

@@ -49,12 +69,12 @@ void StopTLSHelper::start() {
4969

5070
XLOG(DBG3) << "Beginning StopTLS V1 negotiation";
5171
stopTlsFrame_.reset(new apache::thrift::AsyncStopTLS(*this));
52-
// 0ms = no internal timeout. Matches legacy FizzPeeker.cpp convention;
53-
// shutdown lifetime is managed by the owner via cancel().
72+
// AsyncStopTLS arms its own timer when this is non-zero, and routes expiry
73+
// through stopTLSError like any other failure. Zero leaves the exchange
74+
// unbounded, which is only safe for an owner that imposes a deadline of its
75+
// own; nothing here does, so the budget is passed through instead.
5476
stopTlsFrame_->start(
55-
fizzServer_.get(),
56-
apache::thrift::AsyncStopTLS::Role::Server,
57-
std::chrono::milliseconds{0});
77+
fizzServer_.get(), apache::thrift::AsyncStopTLS::Role::Server, timeout_);
5878
}
5979

6080
void StopTLSHelper::stopTLSSuccess(std::unique_ptr<folly::IOBuf> postTLSData) {

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/connection/security/util/StopTLSHelper.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
#pragma once
1818

19+
#include <chrono>
1920
#include <memory>
21+
#include <optional>
2022

2123
#include <fizz/server/AsyncFizzServer.h>
2224
#include <folly/ExceptionWrapper.h>
@@ -36,7 +38,11 @@ namespace apache::thrift::fast_thrift::connection::security::util {
3638
* - Constructed on the EventBase thread that owns the fizz transport. Held
3739
* by the owner (typically StopTLSV1Handler) via a unique_ptr with a
3840
* folly::DelayedDestruction::Destructor deleter.
39-
* - start() kicks off the StopTLS exchange.
41+
* - start() kicks off the StopTLS exchange, bounded by the timeout passed
42+
* at construction. Nothing else bounds it: the exchange completes only
43+
* when the peer answers our close_notify with its own, so without a
44+
* deadline a peer that stops responding holds the transport open until
45+
* it disconnects.
4046
* - On success/error, invokes the user Callback exactly once and then
4147
* synchronously invokes onTerminal(this) so the owner can drop its
4248
* unique_ptr. The deleter triggers destroy(); DelayedDestruction defers
@@ -60,8 +66,13 @@ class StopTLSHelper : private apache::thrift::AsyncStopTLS::Callback,
6066
// Receives `this`; owner uses it to drop its owning UniquePtr.
6167
using OnTerminal = folly::Function<void(StopTLSHelper*) noexcept>;
6268

69+
// timeout bounds the whole exchange; std::nullopt is unbounded. A
70+
// non-positive value is misuse and is logged and treated as unbounded —
71+
// zero reads as "no timeout" to AsyncStopTLS, the opposite of what a zero
72+
// deadline means elsewhere in this pipeline.
6373
StopTLSHelper(
6474
fizz::server::AsyncFizzServer::UniquePtr fizzServer,
75+
std::optional<std::chrono::milliseconds> timeout,
6576
OnTerminal onTerminal,
6677
Callback callback);
6778

@@ -97,6 +108,8 @@ class StopTLSHelper : private apache::thrift::AsyncStopTLS::Callback,
97108
folly::exception_wrapper ex) noexcept;
98109

99110
fizz::server::AsyncFizzServer::UniquePtr fizzServer_;
111+
// Resolved for AsyncStopTLS, whose contract is zero-means-unbounded.
112+
std::chrono::milliseconds timeout_;
100113
apache::thrift::AsyncStopTLS::UniquePtr stopTlsFrame_;
101114
OnTerminal onTerminal_;
102115
Callback callback_;

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/connection/security/util/test/StopTLSHelperTest.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818

1919
#include <sys/socket.h>
2020
#include <array>
21+
#include <chrono>
2122
#include <memory>
23+
#include <optional>
24+
#include <string>
2225

2326
#include <gtest/gtest.h>
2427

2528
#include <fizz/server/AsyncFizzServer.h>
2629
#include <folly/io/async/AsyncSocket.h>
2730
#include <folly/io/async/DelayedDestruction.h>
2831
#include <folly/io/async/ScopedEventBaseThread.h>
32+
#include <folly/synchronization/Baton.h>
2933

3034
#include <thrift/lib/cpp2/fast_thrift/security/FizzServerCertConfig.h>
3135
#include <thrift/lib/cpp2/fast_thrift/security/FizzServerContextBuilder.h>
@@ -97,6 +101,7 @@ TEST_F(StopTLSHelperTest, CancelBeforeStart) {
97101
auto fizzServer = makeFizzServer(sp.server);
98102
current_.reset(new StopTLSHelper(
99103
std::move(fizzServer),
104+
/*timeout=*/std::nullopt,
100105
[this](StopTLSHelper*) noexcept {
101106
++terminalCount_;
102107
current_.reset();
@@ -121,4 +126,56 @@ TEST_F(StopTLSHelperTest, CancelBeforeStart) {
121126
::close(sp.client.toFd());
122127
}
123128

129+
// A peer that never answers our close_notify would otherwise hold the
130+
// transport open until it disconnects — nothing else bounds the exchange.
131+
TEST_F(StopTLSHelperTest, TimesOutWhenPeerNeverAnswers) {
132+
constexpr auto kTimeout = std::chrono::milliseconds{50};
133+
auto sp = makeSocketPair();
134+
folly::Baton<> done;
135+
folly::exception_wrapper timeoutEx;
136+
// Both stamped on the EventBase thread, ordered before done.post().
137+
std::chrono::steady_clock::time_point startedAt;
138+
std::chrono::steady_clock::duration elapsed{};
139+
140+
ASSERT_NE(evb_, nullptr);
141+
evb_->runInEventBaseThreadAndWait([&] {
142+
auto fizzServer = makeFizzServer(sp.server);
143+
current_.reset(new StopTLSHelper(
144+
std::move(fizzServer),
145+
kTimeout,
146+
// Posted from the terminal callback, not the user callback: the
147+
// terminal fires last, so waking the main thread any earlier races it
148+
// against the writes below.
149+
[this, &done, &startedAt, &elapsed](StopTLSHelper*) noexcept {
150+
++terminalCount_;
151+
elapsed = std::chrono::steady_clock::now() - startedAt;
152+
current_.reset();
153+
done.post();
154+
},
155+
[&](folly::AsyncTransport::UniquePtr,
156+
folly::exception_wrapper ex) noexcept {
157+
timeoutEx = std::move(ex);
158+
}));
159+
startedAt = std::chrono::steady_clock::now();
160+
current_->start();
161+
});
162+
163+
ASSERT_TRUE(done.try_wait_for(std::chrono::seconds{5}));
164+
EXPECT_EQ(terminalCount_, 1u);
165+
// Assert on the timer's own error: any other failure would satisfy a bare
166+
// "did it fail" check without proving the deadline fired.
167+
EXPECT_NE(
168+
timeoutEx.what().toStdString().find("timeout expired"), std::string::npos)
169+
<< "expected AsyncStopTLS timeout, got: " << timeoutEx.what();
170+
// Independent of the message: a terminal that arrived before the budget
171+
// elapsed cannot have been driven by the deadline. Only a lower bound is
172+
// checked — the timer is armed after startedAt, and scheduling slack only
173+
// ever pushes the terminal later.
174+
const auto elapsedMs =
175+
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
176+
EXPECT_GE(elapsedMs.count(), kTimeout.count());
177+
178+
::close(sp.client.toFd());
179+
}
180+
124181
} // namespace apache::thrift::fast_thrift::connection::security::util::test

0 commit comments

Comments
 (0)