This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new dd0af2bd Contain malformed Thrift T_EXCEPTION replies on the client 
(#3500)
dd0af2bd is described below

commit dd0af2bd2de309170bce9defee67a40985fac940
Author: Weibing Wang <[email protected]>
AuthorDate: Sat Aug 29 21:42:18 2026 +0800

    Contain malformed Thrift T_EXCEPTION replies on the client (#3500)
    
    * Contain malformed Thrift T_EXCEPTION replies on the client
    
    The Thrift client decodes a T_EXCEPTION reply in ReadThriftException
    without catching what the underlying Thrift library throws. A reply
    carrying a malformed exception struct (e.g. a field with an oversized
    length) makes TBinaryProtocol raise an exception that unwinds through
    ProcessThriftResponse up to the bthread task frame, where std::terminate()
    is called, killing the whole process and every other in-flight RPC on it.
    
    Wrap the decode in the same try/catch that ReadThriftStruct already uses
    for the normal reply body, so a malformed T_EXCEPTION reply degrades to a
    failed parse instead of crashing the process. Add a unit test feeding a
    malformed T_EXCEPTION reply through the client response path.
    
    * Make the thrift T_EXCEPTION test compile on builds without thrift
    
    * Polish the thrift T_EXCEPTION handling test and log messages
    
    Incorporate review feedback:
    - Catch std::exception by const ref and fix 'Catched' -> 'Caught' in the
      T_EXCEPTION reply parse log messages.
    - Add <unistd.h> and close the pipe fds / release the socket in TearDown()
      so the unit test does not leak OS resources across tests.
    
    * Avoid double-close of the pipe write end in the thrift test
    
    The test socket owns the pipe write end passed in SocketOptions::fd and
    closes it on destruction, so TearDown must only close the read end to
    avoid double-closing (and potentially closing a recycled fd).
    
    * Remove redundant thrift define from test build
    
    Unit test targets already inherit ENABLE_THRIFT_FRAMED_PROTOCOL from the
    brpc source target's transitive defines, so the select in test/BUILD.bazel
    is unnecessary.
---
 src/brpc/policy/thrift_protocol.cpp    |  18 +++-
 test/brpc_thrift_protocol_unittest.cpp | 150 +++++++++++++++++++++++++++++++++
 2 files changed, 165 insertions(+), 3 deletions(-)

diff --git a/src/brpc/policy/thrift_protocol.cpp 
b/src/brpc/policy/thrift_protocol.cpp
index dce6bc38..e97dd00b 100755
--- a/src/brpc/policy/thrift_protocol.cpp
+++ b/src/brpc/policy/thrift_protocol.cpp
@@ -184,9 +184,21 @@ void ReadThriftException(const butil::IOBuf& body,
             ::apache::thrift::transport::TMemoryBuffer::TAKE_OWNERSHIP);
     
apache::thrift::protocol::TBinaryProtocolT<apache::thrift::transport::TMemoryBuffer>
 iprot(in_buffer);
 
-    x->read(&iprot);
-    iprot.readMessageEnd();
-    iprot.getTransport()->readEnd();
+    // A malformed exception struct may make the underlying thrift code throw
+    // (e.g. TProtocolException on a bad field or TTransportException /
+    // std::length_error on a bad length). Such an exception must be contained
+    // here: if it propagated out, it would unwind through 
ProcessThriftResponse
+    // up to the bthread task frame and call std::terminate(), taking down the
+    // whole process along with every other in-flight RPC on it.
+    try {
+        x->read(&iprot);
+        iprot.readMessageEnd();
+        iprot.getTransport()->readEnd();
+    } catch (const std::exception& e) {
+        LOG(WARNING) << "Caught thrift exception while parsing T_EXCEPTION 
reply: " << e.what();
+    } catch (...) {
+        LOG(WARNING) << "Caught unknown thrift exception while parsing 
T_EXCEPTION reply";
+    }
 }
 
 // The continuation of request processing. Namely send response back to client.
diff --git a/test/brpc_thrift_protocol_unittest.cpp 
b/test/brpc_thrift_protocol_unittest.cpp
new file mode 100644
index 00000000..095c9a3b
--- /dev/null
+++ b/test/brpc_thrift_protocol_unittest.cpp
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// brpc - A framework to host and access services throughout Baidu.
+
+#include <gtest/gtest.h>
+#include <gflags/gflags.h>
+#include <unistd.h>
+
+int main(int argc, char* argv[]) {
+    testing::InitGoogleTest(&argc, argv);
+    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
+    return RUN_ALL_TESTS();
+}
+
+// The malformed T_EXCEPTION reply is decoded by the Thrift framed protocol,
+// which is only compiled in when the build has THRIFT support enabled (the
+// WITH_THRIFT build flag / brpc_with_thrift config, off by default). Keep this
+// test a trivial pass on builds without Thrift so it links cleanly there too.
+#ifdef ENABLE_THRIFT_FRAMED_PROTOCOL
+
+#include <arpa/inet.h>
+#include "butil/macros.h"
+#include "brpc/socket.h"
+#include "brpc/controller.h"
+#include "brpc/policy/most_common_message.h"
+#include "brpc/policy/thrift_protocol.h"
+
+namespace {
+
+// A nominal thrift message-type value for a T_EXCEPTION reply.
+static const uint32_t THRIFT_TYPE_EXCEPTION = 3;
+static const uint32_t THRIFT_HEAD_VERSION_1 = 0x80010000;
+
+class ThriftProtocolTest : public ::testing::Test {
+protected:
+    ThriftProtocolTest() {
+        _pipe_fds[0] = _pipe_fds[1] = -1;
+        EXPECT_EQ(0, pipe(_pipe_fds));
+        brpc::SocketId id;
+        brpc::SocketOptions options;
+        options.fd = _pipe_fds[1];
+        EXPECT_EQ(0, brpc::Socket::Create(options, &id));
+        EXPECT_EQ(0, brpc::Socket::Address(id, &_socket));
+    }
+
+    virtual void SetUp() {}
+    virtual void TearDown() {
+        // The test socket owns `_pipe_fds[1]` (SocketOptions::fd takes
+        // ownership and Socket closes it on destruction), so only the read
+        // end `_pipe_fds[0]` is closed here to avoid double-close.
+        if (_pipe_fds[0] >= 0) {
+            close(_pipe_fds[0]);
+            _pipe_fds[0] = -1;
+        }
+        _pipe_fds[1] = -1;
+        _socket.reset();
+    }
+
+    // Attach the test socket to the message so that ProcessThriftResponse
+    // can read the correlation_id out of it.
+    void AttachMessage(brpc::InputMessageBase* msg) {
+        if (msg->_socket == nullptr) {
+            _socket->ReAddress(&msg->_socket);
+        }
+    }
+
+    // Build a MostCommonMessage whose payload is a thrift T_EXCEPTION reply
+    // whose exception struct is malformed (a T_STRING field whose length is
+    // huge and never bounded by real bytes).
+    brpc::policy::MostCommonMessage* MakeMalformedExceptionMessage() {
+        const char method_name[] = "echo";
+        const uint32_t version_and_type = htonl(
+            THRIFT_HEAD_VERSION_1 | THRIFT_TYPE_EXCEPTION);
+        const uint32_t method_name_length = htonl(sizeof(method_name) - 1);
+        const uint32_t seq_id = htonl(0);
+        // Malformed exception struct body: one field, id 1, type T_STRING,
+        // string length 0x7FFFFFFF and no following bytes.
+        static const uint8_t malformed_body[] =
+            { 0x0B, 0x00, 0x01, 0x7F, 0xFF, 0xFF, 0xFF };
+
+        brpc::policy::MostCommonMessage* msg =
+                brpc::policy::MostCommonMessage::Get();
+        msg->payload.append(&version_and_type, sizeof(version_and_type));
+        msg->payload.append(&method_name_length, sizeof(method_name_length));
+        msg->payload.append(method_name, sizeof(method_name) - 1);
+        msg->payload.append(&seq_id, sizeof(seq_id));
+        msg->payload.append(malformed_body, sizeof(malformed_body));
+        return msg;
+    }
+
+    int _pipe_fds[2];
+    brpc::SocketUniquePtr _socket;
+};
+
+TEST_F(ThriftProtocolTest, malformed_exception_reply_does_not_crash_client) {
+    brpc::Controller cntl;
+    // Simulate a real client: the socket's correlation_id matches the
+    // controller's call_id so that ProcessThriftResponse can find the
+    // controller.
+    _socket->set_correlation_id(cntl.call_id().value);
+
+    brpc::policy::MostCommonMessage* msg = MakeMalformedExceptionMessage();
+    AttachMessage(msg);
+
+    // Before the fix, ReadThriftException lets the malformed reply unwind out
+    // of ProcessThriftResponse as an uncaught thrift exception (in production
+    // that reaches the bthread task frame and calls std::terminate). After the
+    // fix it must be contained and reported as a normal RPC failure instead.
+    EXPECT_NO_THROW(brpc::policy::ProcessThriftResponse(msg));
+}
+
+TEST_F(ThriftProtocolTest, malformed_exception_reply_sets_failed) {
+    brpc::Controller cntl;
+    _socket->set_correlation_id(cntl.call_id().value);
+
+    brpc::policy::MostCommonMessage* msg = MakeMalformedExceptionMessage();
+    AttachMessage(msg);
+    brpc::policy::ProcessThriftResponse(msg);
+    // After the fix the malformed exception reply must be surfaced to the
+    // caller as a failed RPC rather than crashing the process.
+    EXPECT_TRUE(cntl.Failed());
+}
+
+}  // namespace
+
+#else
+
+namespace {
+// Trivial placeholder so the test links successfully on builds without
+// THRIFT support (the guarded code above is compiled out).
+class ThriftProtocolTest : public ::testing::Test {};
+TEST_F(ThriftProtocolTest, skipped_without_thrift_support) {}
+}  // namespace
+
+#endif


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to