Skip to content

Commit 1cf2c09

Browse files
author
shengtiedan
committed
fix _read_buf race between PollCq and OnNewMessages in RDMA server
The server-side RDMA socket's _read_buf is accessed by two independent bthreads: PollCq (CQ socket) writes RDMA data via HandleCompletion and calls ProcessNewMessage (which reads _read_buf via CutInputMessage), and OnNewMessages (main socket) reads TCP data for handshake / fallback. Since IOBuf is not thread-safe, concurrent access corrupts internal state and causes intermittent core dumps. Three fixes: 1. Switch edge trigger to OnNewDataFromTcp in ALL ExecuteServerHandshake end paths (ESTABLISHED + 5 failure paths). OnNewDataFromTcp checks the RDMA state: in ESTABLISHED it only reads 1 byte for EOF detection without touching _read_buf; in FALLBACK_TCP it delegates to OnNewMessages for TCP data. This prevents post-handshake races. 2. Guard HandleCompletion (IBV_WC_RECV) with a state check: skip writing to _read_buf if the state is not ESTABLISHED, but still handle imm data, re-post the recv WR (with failure check), and send ack. This prevents races during the handshake (after BringUpQp puts the QP into RTS, the client may start sending RDMA data before the server finishes processing the ACK). 3. Remove the source->size() > HELLO_ACK_LEN check in Phase 2. When a client falls back to TCP, the 4-byte ACK and the first RPC request may arrive in the same readv() call. Use cutn() to drain the 4-byte ACK and let remaining data be processed by other parsers, matching FallbackServerHandshake's behavior. Additionally: - Return NOT_ENOUGH_DATA (not TRY_OTHERS) from the ESTABLISHED path so CutInputMessage returns immediately without reading _read_buf, minimizing the race window with PollCq. - Clear _read_buf before transitioning to ESTABLISHED so that residual TCP data cannot become a prefix of the RDMA recv stream (HandleCompletion appends to _read_buf, not overwrites). The clear is safe because HandleCompletion only writes after seeing ESTABLISHED (acquire), which is stored (release) strictly after the clear. - Use memory_order_release for ESTABLISHED stores (both client and server) to properly pair with the acquire load in HandleCompletion. - Restore edge trigger in RdmaTransport::Reset() based on CreatedByConnect(): OnNewDataFromTcp for client-side sockets, OnNewMessages for server-side sockets, matching the logic in Init(). - Add Transport::ShouldStopReading() virtual method (default false), overridden by RdmaTransport to return true when the RDMA endpoint has reached ESTABLISHED (via RdmaEndpoint::IsEstablished(), which uses an acquire load on the already-atomic _state, pairing with the release store in ExecuteServerHandshake). OnNewMessages checks this after ProcessNewMessage returns and exits immediately, preventing it from calling DoRead again on _read_buf after the edge trigger has been switched. - Guard ProcessNewMessage in PollCq with bytes > 0: when bytes == 0 (IBV_WC_SEND completions, or IBV_WC_RECV dropped during handshake), skip ProcessNewMessage entirely. This prevents PollCq from calling CutInputMessage on _read_buf (via ProcessNewMessage) while OnNewMessages is driving the handshake on the same _read_buf.
1 parent f90ab52 commit 1cf2c09

6 files changed

Lines changed: 92 additions & 23 deletions

File tree

src/brpc/input_messenger.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,19 @@ void InputMessenger::OnNewMessages(Socket* m) {
369369
if (messenger->ProcessNewMessage(m, nr, read_eof, received_us,
370370
base_realtime, last_msg) < 0) {
371371
return;
372-
}
372+
}
373+
// If the transport switched its edge trigger during parsing (e.g.,
374+
// RDMA handshake completed and edge trigger changed to
375+
// OnNewDataFromTcp), stop reading to avoid racing with the new
376+
// edge trigger handler on _read_buf. Drain _nevent so future
377+
// epoll events can schedule the new edge trigger handler.
378+
if (m->_transport->ShouldStopReading()) {
379+
while (m->MoreReadEvents(&progress)) {}
380+
if (read_eof) {
381+
m->SetEOF();
382+
}
383+
return;
384+
}
373385
}
374386

375387
if (read_eof) {

src/brpc/rdma/rdma_endpoint.cpp

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ void* RdmaEndpoint::ProcessHandshakeAtClient(void* arg) {
500500
}
501501

502502
if (rdma_transport->_rdma_state == RdmaTransport::RDMA_ON) {
503-
ep->_state.store(ESTABLISHED, butil::memory_order_relaxed);
503+
ep->_state.store(ESTABLISHED, butil::memory_order_release);
504504
LOG_IF(INFO, FLAGS_rdma_trace_verbose)
505505
<< "Client handshake ends (use rdma v" << ep->_handshake_version
506506
<< ") on " << s->description();
@@ -560,6 +560,7 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s
560560
}
561561
if (r == RemoteHelloResult::ERROR) {
562562
ep->_state.store(FAILED, butil::memory_order_relaxed);
563+
rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
563564
return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG);
564565
}
565566

@@ -592,6 +593,7 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s
592593
if (hs->SendLocalHello() < 0) {
593594
PLOG(WARNING) << "Fail to send server hello to " << s->description();
594595
ep->_state.store(FAILED, butil::memory_order_relaxed);
596+
rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
595597
return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG);
596598
}
597599

@@ -607,13 +609,6 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s
607609
if (source->size() < HELLO_ACK_LEN) {
608610
return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA);
609611
}
610-
if (source->size() > HELLO_ACK_LEN) {
611-
LOG(WARNING) << "Too many bytes in handshake ACK, drop connection: "
612-
<< s->description();
613-
ep->_state.store(FAILED, butil::memory_order_relaxed);
614-
s->reset_parsing_context(nullptr);
615-
return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG);
616-
}
617612

618613
uint32_t flags_be = 0;
619614
CHECK_EQ(source->cutn(&flags_be, HELLO_ACK_LEN), HELLO_ACK_LEN);
@@ -625,6 +620,7 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s
625620
rdma_transport->_rdma_state = RdmaTransport::RDMA_OFF;
626621
ep->_state.store(FALLBACK_TCP, butil::memory_order_release);
627622
s->reset_parsing_context(nullptr);
623+
rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
628624
return MakeParseError(PARSE_ERROR_TRY_OTHERS);
629625
}
630626

@@ -633,16 +629,33 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s
633629
<< s->description();
634630
ep->_state.store(FAILED, butil::memory_order_relaxed);
635631
s->reset_parsing_context(nullptr);
632+
rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
636633
return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG);
637634
}
638635

639636
LOG_IF(INFO, FLAGS_rdma_trace_verbose)
640637
<< "Server handshake ends (use rdma v" << ep->_handshake_version
641638
<< ") on " << s->description();
642639
rdma_transport->_rdma_state = RdmaTransport::RDMA_ON;
643-
ep->_state.store(ESTABLISHED, butil::memory_order_relaxed);
640+
// Clear any residual TCP data so it cannot pollute the RDMA recv
641+
// stream. HandleCompletion appends (not overwrites) to _read_buf,
642+
// so leftover bytes would become a prefix to RDMA data and break
643+
// parsing. This clear is safe because HandleCompletion only writes
644+
// _read_buf after seeing ESTABLISHED (acquire), which is stored
645+
// below (release) — strictly after this clear.
646+
source->clear();
647+
ep->_state.store(ESTABLISHED, butil::memory_order_release);
644648
s->reset_parsing_context(nullptr);
645-
return MakeParseError(PARSE_ERROR_TRY_OTHERS);
649+
rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
650+
// Return NOT_ENOUGH_DATA (not TRY_OTHERS) so that CutInputMessage
651+
// returns immediately without invoking other parsers on _read_buf.
652+
// With TRY_OTHERS, each parser would call source->size() on _read_buf,
653+
// racing with HandleCompletion which may be appending RDMA data to
654+
// _read_buf concurrently. The preferred_index is left as the handshake
655+
// parser, but this is self-correcting: the first PollCq-driven
656+
// ProcessNewMessage will try the handshake parser, get TRY_OTHERS
657+
// (magic mismatch), and switch to the correct parser.
658+
return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA);
646659
}
647660

648661
bool RdmaEndpoint::IsWritable() const {
@@ -916,16 +929,31 @@ ssize_t RdmaEndpoint::HandleCompletion(ibv_wc& wc) {
916929
}
917930
case IBV_WC_RECV: { // recv completion
918931
// Please note that only the first wc.byte_len bytes is valid
932+
ssize_t bytes_written = 0;
919933
if (wc.byte_len > 0) {
920934
if (wc.byte_len < (uint32_t)FLAGS_rdma_zerocopy_min_size) {
921935
zerocopy = false;
922936
}
923-
CHECK_NE(_state.load(butil::memory_order_relaxed), FALLBACK_TCP);
924-
if (zerocopy) {
925-
_rbuf[_rq_received].cutn(&_socket->_read_buf, wc.byte_len);
937+
// Don't write to _read_buf until the handshake is fully done
938+
// (ESTABLISHED). During the handshake (S_ACK_WAIT etc.), the
939+
// main socket's OnNewMessages is driving the handshake via
940+
// _read_buf; PollCq writing to _read_buf concurrently corrupts
941+
// the IOBuf (non-thread-safe). Fall through to handle imm
942+
// data, re-post recv WR, and send ack normally.
943+
if (_state.load(butil::memory_order_acquire) != ESTABLISHED) {
944+
LOG_EVERY_N(WARNING, 100)
945+
<< "RDMA recv completion in non-ESTABLISHED state "
946+
<< GetStateStr() << ", drop "
947+
<< wc.byte_len << " bytes from "
948+
<< _socket->description();
926949
} else {
927-
// Copy data when the receive data is really small
928-
_socket->_read_buf.append(_rbuf_data[_rq_received], wc.byte_len);
950+
if (zerocopy) {
951+
_rbuf[_rq_received].cutn(&_socket->_read_buf, wc.byte_len);
952+
} else {
953+
// Copy data when the receive data is really small
954+
_socket->_read_buf.append(_rbuf_data[_rq_received], wc.byte_len);
955+
}
956+
bytes_written = wc.byte_len;
929957
}
930958
}
931959
if (0 != (wc.wc_flags & IBV_WC_WITH_IMM) && wc.imm_data > 0) {
@@ -948,7 +976,7 @@ ssize_t RdmaEndpoint::HandleCompletion(ibv_wc& wc) {
948976
if (wc.byte_len > 0) {
949977
SendAck(1);
950978
}
951-
return wc.byte_len;
979+
return bytes_written;
952980
}
953981
default:
954982
// Some driver bugs may lead to unexpected completion opcode.
@@ -1593,12 +1621,19 @@ void RdmaEndpoint::PollCq(Socket* m) {
15931621

15941622
// Just call PrcessNewMessage once for all of these CQEs.
15951623
// Otherwise it may call too many bthread_flush to affect performance.
1596-
const int64_t received_us = butil::cpuwide_time_us();
1597-
const int64_t base_realtime = butil::gettimeofday_us() - received_us;
1598-
InputMessenger* messenger = static_cast<InputMessenger*>(s->user());
1599-
if (messenger->ProcessNewMessage(
1600-
s.get(), bytes, false, received_us, base_realtime, last_msg) < 0) {
1601-
return;
1624+
// Only call when bytes > 0: when bytes == 0, HandleCompletion wrote
1625+
// nothing to _read_buf (e.g., IBV_WC_SEND completions, or IBV_WC_RECV
1626+
// dropped during handshake). Calling ProcessNewMessage with bytes == 0
1627+
// would still invoke CutInputMessage on _read_buf, racing with
1628+
// OnNewMessages which is driving the handshake via the same _read_buf.
1629+
if (bytes > 0) {
1630+
const int64_t received_us = butil::cpuwide_time_us();
1631+
const int64_t base_realtime = butil::gettimeofday_us() - received_us;
1632+
InputMessenger* messenger = static_cast<InputMessenger*>(s->user());
1633+
if (messenger->ProcessNewMessage(
1634+
s.get(), bytes, false, received_us, base_realtime, last_msg) < 0) {
1635+
return;
1636+
}
16021637
}
16031638
}
16041639
}

src/brpc/rdma/rdma_endpoint.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ friend int v3_wire::WriteV3Hello(RdmaEndpoint*, const RdmaHello&);
124124
// Whether the endpoint can send more data
125125
bool IsWritable() const;
126126

127+
// Whether the RDMA handshake has reached ESTABLISHED.
128+
// Uses acquire load to pair with the release store in
129+
// ExecuteServerHandshake / ProcessHandshakeAtClient.
130+
bool IsEstablished() const {
131+
return _state.load(butil::memory_order_acquire) == ESTABLISHED;
132+
}
133+
127134
// For debug
128135
void DebugInfo(std::ostream& os,
129136
butil::StringPiece connector = "\n") const;

src/brpc/rdma_transport.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,19 @@ int RdmaTransport::Reset(int32_t expected_nref) {
7070
if (_rdma_ep) {
7171
_rdma_ep->Reset();
7272
_rdma_state = RDMA_UNKNOWN;
73+
if (_socket->CreatedByConnect()) {
74+
_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp;
75+
} else {
76+
_on_edge_trigger = InputMessenger::OnNewMessages;
77+
}
7378
}
7479
return 0;
7580
}
7681

82+
bool RdmaTransport::ShouldStopReading() const {
83+
return _rdma_ep && _rdma_ep->IsEstablished();
84+
}
85+
7786
std::shared_ptr<AppConnect> RdmaTransport::Connect() {
7887
if (_default_connect == nullptr) {
7988
return std::make_shared<rdma::RdmaConnect>();

src/brpc/rdma_transport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ friend class rdma::RdmaHandshakeServerV3;
4141
void ProcessEvent(bthread_attr_t attr) override;
4242
void QueueMessage(InputMessageClosure& inputMsg, int* num_bthread_created, bool last_msg) override;
4343
void Debug(std::ostream &os) override;
44+
bool ShouldStopReading() const override;
4445
rdma::RdmaEndpoint* GetRdmaEp() {
4546
CHECK(_rdma_ep != nullptr);
4647
return _rdma_ep;

src/brpc/transport.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class Transport {
5151
virtual void QueueMessage(InputMessageClosure& input_msg, int* num_bthread_created, bool last_msg) = 0;
5252
virtual void Debug(std::ostream &os) = 0;
5353

54+
// Returns true if OnNewMessages should stop its read loop immediately
55+
// (e.g., RDMA transport after handshake completes and edge trigger
56+
// is switched to OnNewDataFromTcp). Default: never stop.
57+
virtual bool ShouldStopReading() const { return false; }
58+
5459
bool HasOnEdgeTrigger() {
5560
return _on_edge_trigger != nullptr;
5661
}

0 commit comments

Comments
 (0)