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 2621a260 Fix bthread_id lock leak in ProcessNsheadMcpackResponse 
(#3574)
2621a260 is described below

commit 2621a26088897ec904e8483ef0ea68c6ad5a1131
Author: Weibing Wang <[email protected]>
AuthorDate: Thu Oct 1 18:02:40 2026 +0800

    Fix bthread_id lock leak in ProcessNsheadMcpackResponse (#3574)
    
    * Fix bthread_id lock leak in ProcessNsheadMcpackResponse
    
    After ProcessNsheadMcpackResponse acquires the bthread_id lock, its two
    early-return paths (response object missing, mcpack parse failure) skip
    accessor.OnResponse(), the only unlock entry. The leaked lock makes
    Join(correlation_id) hang forever and the RPC timeout cannot rescue it
    since the timeout error is only consumed during unlocking. A server
    returning a valid nshead header plus a malformed mcpack body reliably
    triggers this.
    
    Adopt the do { ... break ... } while(0) pattern (consistent with
    ProcessRpcResponse) so that OnResponse() is always reached.
    
    * Close unused pipe read end in nshead_mcpack unittest fixture
    
    * Fix compile error with new protobuf in nshead_mcpack unittest
    
    Descriptor::full_name() returns absl::string_view since protobuf 35.x,
    which cannot be implicitly converted to the const std::string& parameter
    of mcpack2pb::register_message_handler_or_die. Wrap it into std::string
    explicitly.
    
    ---------
    
    Co-authored-by: brpc-oncall <brpc-oncall@localhost>
---
 src/brpc/policy/nshead_mcpack_protocol.cpp    |  23 +--
 test/brpc_nshead_mcpack_protocol_unittest.cpp | 198 ++++++++++++++++++++++++++
 2 files changed, 211 insertions(+), 10 deletions(-)

diff --git a/src/brpc/policy/nshead_mcpack_protocol.cpp 
b/src/brpc/policy/nshead_mcpack_protocol.cpp
index d242808f..bad8770b 100644
--- a/src/brpc/policy/nshead_mcpack_protocol.cpp
+++ b/src/brpc/policy/nshead_mcpack_protocol.cpp
@@ -119,16 +119,19 @@ void ProcessNsheadMcpackResponse(InputMessageBase* 
msg_base) {
         span->set_start_parse_us(start_parse_us);
     }
     const int saved_error = cntl->ErrorCode();
-    google::protobuf::Message* res = cntl->response();
-    if (res == nullptr) {
-        // silently ignore response.
-        return;
-    }
-    const std::string msg_name = 
butil::EnsureString(res->GetDescriptor()->full_name());
-    mcpack2pb::MessageHandler handler = 
mcpack2pb::find_message_handler(msg_name);
-    if (!handler.parse_from_iobuf(res, msg->payload)) {
-        return cntl->CloseConnection("Fail to parse response message");
-    }
+    do {
+        google::protobuf::Message* res = cntl->response();
+        if (res == nullptr) {
+            // silently ignore response.
+            break;
+        }
+        const std::string msg_name = 
butil::EnsureString(res->GetDescriptor()->full_name());
+        mcpack2pb::MessageHandler handler = 
mcpack2pb::find_message_handler(msg_name);
+        if (!handler.parse_from_iobuf(res, msg->payload)) {
+            cntl->CloseConnection("Fail to parse response message");
+            break;
+        }
+    } while (0);
     // Unlocks correlation_id inside. Revert controller's
     // error code if it version check of `cid' fails
     msg.reset();  // optional, just release resource ASAP
diff --git a/test/brpc_nshead_mcpack_protocol_unittest.cpp 
b/test/brpc_nshead_mcpack_protocol_unittest.cpp
new file mode 100644
index 00000000..953bd1f7
--- /dev/null
+++ b/test/brpc_nshead_mcpack_protocol_unittest.cpp
@@ -0,0 +1,198 @@
+// 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.
+
+// Unit tests of the nshead_mcpack protocol, focused on the client-side
+// response handler: `ProcessNsheadMcpackResponse' must always release the
+// bthread_id lock (via `accessor.OnResponse') on every path, otherwise the
+// RPC caller hangs on Join() forever and even the timeout mechanism cannot
+// rescue it.
+
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <gtest/gtest.h>
+#include <gflags/gflags.h>
+#include <google/protobuf/descriptor.h>
+#include "butil/time.h"
+#include "butil/macros.h"
+#include "bthread/id.h"
+#include "brpc/socket.h"
+#include "brpc/acceptor.h"
+#include "brpc/server.h"
+#include "brpc/nshead.h"
+#include "brpc/controller.h"
+#include "brpc/policy/nshead_mcpack_protocol.h"
+#include "brpc/policy/nshead_protocol.h"
+#include "brpc/policy/most_common_message.h"
+#include "mcpack2pb/mcpack2pb.h"
+#include "echo.pb.h"
+
+namespace {
+
+static const std::string EXP_RESPONSE = "world";
+
+// Simulates the mcpack parser facing a garbage body: drains the input and
+// returns 0 so that `parse_from_iobuf' reports failure.
+static size_t FailingParse(google::protobuf::Message*,
+                           google::protobuf::io::ZeroCopyInputStream* input) {
+    const void* data;
+    int size;
+    while (input->Next(&data, &size)) {}
+    return 0;
+}
+
+// Simulates a successful mcpack parsing which fills the response.
+static size_t SuccessfulParse(google::protobuf::Message* msg,
+                              google::protobuf::io::ZeroCopyInputStream* 
input) {
+    const void* data;
+    int size;
+    size_t total = 0;
+    while (input->Next(&data, &size)) {
+        total += size;
+    }
+    static_cast<test::EchoRequest*>(msg)->set_message(EXP_RESPONSE);
+    return total;
+}
+
+static void RegisterMcpackHandlers() {
+    static bool registered = []() {
+        mcpack2pb::MessageHandler failing_handler = {
+            FailingParse, nullptr, nullptr, nullptr};
+        // Note: `full_name()' returns absl::string_view in newer protobuf,
+        // wrap it into std::string explicitly.
+        mcpack2pb::register_message_handler_or_die(
+            std::string(test::EchoResponse::descriptor()->full_name()), 
failing_handler);
+        mcpack2pb::MessageHandler working_handler = {
+            SuccessfulParse, nullptr, nullptr, nullptr};
+        mcpack2pb::register_message_handler_or_die(
+            std::string(test::EchoRequest::descriptor()->full_name()), 
working_handler);
+        return true;
+    }();
+    (void)registered;
+}
+
+class NsheadMcpackTest : public ::testing::Test {
+protected:
+    NsheadMcpackTest() {
+        RegisterMcpackHandlers();
+        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 ~NsheadMcpackTest() {
+        // The write end (_pipe_fds[1]) is owned by `_socket' and closed
+        // when it is released; close the unused read end here to avoid
+        // leaking fds across the per-case fixtures.
+        close(_pipe_fds[0]);
+    };
+    virtual void SetUp() {};
+    virtual void TearDown() {};
+
+    void ProcessMessage(brpc::InputMessageBase* msg) {
+        if (msg->_socket == nullptr) {
+            _socket->ReAddress(&msg->_socket);
+        }
+        _socket->PostponeEOF();
+        brpc::policy::ProcessNsheadMcpackResponse(msg);
+    }
+
+    // Make a response with a valid nshead header followed by a garbage
+    // body which cannot be parsed as an mcpack message.
+    brpc::policy::MostCommonMessage* MakeMalformedResponseMessage() {
+        brpc::policy::MostCommonMessage* msg =
+                brpc::policy::MostCommonMessage::Get();
+        brpc::nshead_t head;
+        memset(&head, 0, sizeof(head));
+        head.magic_num = brpc::NSHEAD_MAGICNUM;
+        const char garbage[] = "\xde\xad\xbe\xef";
+        head.body_len = sizeof(garbage) - 1;
+        msg->meta.append(&head, sizeof(head));
+        msg->payload.append(garbage, sizeof(garbage) - 1);
+        return msg;
+    }
+
+    // Assert that the correlation_id bound to the RPC is no longer locked.
+    // If the lock was leaked, `brpc::Join' in user code would hang forever
+    // and even the RPC timeout could not rescue it, because the timeout
+    // error is only consumed during unlocking.
+    void AssertLockReleased(const brpc::CallId& cid) {
+        // A locked id returns EBUSY while a destroyed (released) id
+        // returns EINVAL.
+        ASSERT_EQ(EINVAL, bthread_id_trylock(cid, nullptr))
+            << "correlation_id lock was leaked";
+    }
+
+    int _pipe_fds[2];
+    brpc::SocketUniquePtr _socket;
+};
+
+// A malformed mcpack body must fail the RPC instead of leaking the lock.
+TEST_F(NsheadMcpackTest, process_response_with_malformed_body) {
+    test::EchoResponse res;
+    brpc::Controller cntl;
+    cntl._response = &res;
+    brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
+    _socket->set_correlation_id(cntl.call_id().value);
+    ProcessMessage(msg);
+    ASSERT_TRUE(cntl.Failed());
+    AssertLockReleased(cntl.call_id());
+}
+
+// A response message whose mcpack handler was never registered must not
+// leak the lock either.
+TEST_F(NsheadMcpackTest, process_response_with_unregistered_handler) {
+    // test::ComboRequest has no registered mcpack handler at all.
+    test::ComboRequest res;
+    brpc::Controller cntl;
+    cntl._response = &res;
+    brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
+    _socket->set_correlation_id(cntl.call_id().value);
+    ProcessMessage(msg);
+    ASSERT_TRUE(cntl.Failed());
+    AssertLockReleased(cntl.call_id());
+}
+
+// The controller without a response object must not leak the lock.
+TEST_F(NsheadMcpackTest, process_response_without_response_object) {
+    brpc::Controller cntl;
+    brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
+    _socket->set_correlation_id(cntl.call_id().value);
+    ProcessMessage(msg);
+    AssertLockReleased(cntl.call_id());
+}
+
+// A well-formed response must be delivered to the user and unlock normally.
+TEST_F(NsheadMcpackTest, process_response_success) {
+    // Reuse test::EchoRequest whose registered handler always succeeds.
+    test::EchoRequest res;
+    brpc::Controller cntl;
+    cntl._response = &res;
+    brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
+    _socket->set_correlation_id(cntl.call_id().value);
+    ProcessMessage(msg);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+    ASSERT_EQ(EXP_RESPONSE, res.message());
+    AssertLockReleased(cntl.call_id());
+}
+
+} //namespace


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

Reply via email to