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 962743de Fix inconsistent ownership handling in
SelectiveChannel::AddChannel on failure (#3595)
962743de is described below
commit 962743de08bfaa5cfb98a9339eaea673dba601a5
Author: NeutralMilkHotel <[email protected]>
AuthorDate: Fri Oct 23 17:45:12 2026 +0800
Fix inconsistent ownership handling in SelectiveChannel::AddChannel on
failure (#3595)
* Fix inconsistent ownership handling in SelectiveChannel::AddChannel on
failure (#1408)
* Fix double-free of sub channel on SelectiveChannel fake socket creation
failure
* Ensure SocketUser is cleaned up when Socket creation fails at IOEvent init
* Fix double-free of SubPartitionChannel on SelectiveChannel::AddChannel
failure (#1408)
* Update user attachment logic on IOEvent init failure
Attach user before returning on IOEvent initialization failure to ensure
proper cleanup.
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
* Fix SelectiveChannel::AddChannel ownership handling on failure
* Fix sub channel leak when SelectiveChannel::AddChannel is called before
Init
* Use ScopeGuard to centralise early-failure cleanup in
ChannelBalancer::AddChannel
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
src/brpc/partition_channel.cpp | 5 +-
src/brpc/selective_channel.cpp | 31 ++++++++++-
src/brpc/selective_channel.h | 8 +++
src/brpc/socket.h | 7 +++
test/brpc_channel_unittest.cpp | 120 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 168 insertions(+), 3 deletions(-)
diff --git a/src/brpc/partition_channel.cpp b/src/brpc/partition_channel.cpp
index 80633af2..28a6dd1a 100644
--- a/src/brpc/partition_channel.cpp
+++ b/src/brpc/partition_channel.cpp
@@ -313,7 +313,10 @@ public:
if (_schan->AddChannel(pchan, &pchan->handle) != 0) {
LOG(ERROR) << "Fail to add SubPartitionChannel=#"
<< part.num_partition_kinds;
- delete pchan;
+ // pchan is added with the default OWNS_CHANNEL: on failure
+ // AddChannel deletes pchan (at the call site or via
+ // SubChannel::BeforeRecycle), so do not delete it here
+ // (that would double-free).
continue;
}
_part_chan_map[part.num_partition_kinds] = pchan;
diff --git a/src/brpc/selective_channel.cpp b/src/brpc/selective_channel.cpp
index d670a947..5d2fbe89 100644
--- a/src/brpc/selective_channel.cpp
+++ b/src/brpc/selective_channel.cpp
@@ -24,6 +24,7 @@
#include "brpc/details/controller_private_accessor.h" // RPCSender
#include "brpc/errno.pb.h" // ERESPONSE
#include "brpc/nonreflectable_message.h"
+#include "butil/memory/scope_guard.h"
#include "brpc/selective_channel.h"
#include "brpc/global.h"
@@ -188,19 +189,30 @@ int ChannelBalancer::AddChannel(ChannelBase* sub_channel,
}
BAIDU_SCOPED_LOCK(_mutex);
if (_chan_map.find(sub_channel) != _chan_map.end()) {
+ // Duplicate: no new registration is created. Ownership is decided by
+ // the first successful registration, so ignore this call's ownership,
+ // don't delete (still referenced by the live registration) and don't
+ // reassign (a failing call should have no side effect).
LOG(ERROR) << "Duplicated sub_channel=" << sub_channel;
return -1;
}
SubChannel* sub_chan = new SubChannel;
sub_chan->chan = sub_channel;
sub_chan->ownership = subopt.ownership;
+ // The socket is deliberately left user-detached during Create so that
+ // any early failure path never triggers BeforeRecycle. This scope guard
+ // centralises the matching cleanup for those paths. It is dismissed
+ // right after set_user below, where ownership fully transfers to the
+ // socket and subsequent failures rely on SetFailed-driven recycling.
+ auto cleanup_subchan = butil::MakeScopeGuard([&]() noexcept {
+ sub_chan->BeforeRecycle(nullptr);
+ });
+
SocketId sock_id;
SocketOptions options;
- options.user = sub_chan;
options.health_check_interval_s = FLAGS_channel_check_interval;
if (Socket::Create(options, &sock_id) != 0) {
- delete sub_chan;
LOG(ERROR) << "Fail to create fake socket for sub channel";
return -1;
}
@@ -210,9 +222,19 @@ int ChannelBalancer::AddChannel(ChannelBase* sub_channel,
LOG(ERROR) << "Fail to address SocketId=" << sock_id;
return -1;
}
+ // The socket is fully created and referenced by `ptr'. Attach sub_chan
+ // now; from here on, failures SetFailed() the socket and its recycling
+ // runs SubChannel::BeforeRecycle, deleting sub_chan (and chan iff
+ // OWNS_CHANNEL).
+ ptr->set_user(sub_chan);
+ cleanup_subchan.dismiss();
if (rc > 0 && !ptr->HCEnabled()) {
LOG(ERROR) << "Health check of SocketId="
<< sock_id << " is disabled";
+ // sub_chan will be deleted when the socket is recycled.
+ ptr->SetFailed();
+ // Cancel health checking.
+ ptr->ReleaseHCRelatedReference();
return -1;
}
if (!AddServer(ServerId(sock_id, subopt.tag))) {
@@ -573,6 +595,11 @@ int SelectiveChannel::AddChannel(ChannelBase* sub_channel,
static_cast<schan::ChannelBalancer*>(_chan._lb.get());
if (lb == nullptr) {
LOG(ERROR) << "You must call Init() to initialize a SelectiveChannel";
+ // The ownership contract applies to every failure: take over an
+ // OWNS_CHANNEL sub channel even though it was never registered.
+ if (option.ownership == OWNS_CHANNEL) {
+ delete sub_channel;
+ }
return -1;
}
return lb->AddChannel(sub_channel, option, handle);
diff --git a/src/brpc/selective_channel.h b/src/brpc/selective_channel.h
index a728ac5e..576b71fd 100644
--- a/src/brpc/selective_channel.h
+++ b/src/brpc/selective_channel.h
@@ -72,6 +72,14 @@ public:
// Add a sub channel, which will be deleted along with schan or explicitly
// by RemoveAndDestroyChannel.
// On success, handle is set with the key for removal.
+ //
+ // Ownership (single source of truth: the FIRST successful registration):
+ // - OWNS_CHANNEL: schan owns sub_channel; caller must not delete it.
+ // - DOESNT_OWN_CHANNEL: caller owns sub_channel and must delete it.
+ // On failure the same rule applies to the registration being created:
schan
+ // deletes sub_channel iff its ownership is OWNS_CHANNEL.
+ // A duplicate sub_channel returns -1 and changes nothing; it stays owned
as
+ // its first registration decided.
// NOTE: Different from pchan, schan can add channels at any time.
// Returns 0 on success, -1 otherwise.
int AddChannel(ChannelBase* sub_channel, ChannelHandle* handle) {
diff --git a/src/brpc/socket.h b/src/brpc/socket.h
index da701e9a..94f85880 100644
--- a/src/brpc/socket.h
+++ b/src/brpc/socket.h
@@ -493,6 +493,13 @@ public:
// `user' parameter passed to Create().
SocketUser* user() const { return _user; }
+ // Attach the SocketUser after creation. Only safe before the SocketId
+ // is published to other threads (e.g. right after a successful
+ // Create+Address). Callers creating a socket with a null user and
+ // attaching it here get a single cleanup rule: creation failures never
+ // run SocketUser::BeforeRecycle, so the call site disposes the user.
+ void set_user(SocketUser* user) { _user = user; }
+
// `conn' parameter passed to Create()
void set_conn(SocketConnection* conn) { _conn = conn; }
SocketConnection* conn() const { return _conn; }
diff --git a/test/brpc_channel_unittest.cpp b/test/brpc_channel_unittest.cpp
index 3663efa6..f6155cbd 100644
--- a/test/brpc_channel_unittest.cpp
+++ b/test/brpc_channel_unittest.cpp
@@ -3966,4 +3966,124 @@ TEST_F(RateLimitedBackupPolicyTest,
OnRPCEndDrivesRatioDownAndReAllows) {
ASSERT_TRUE(p->DoBackup(nullptr));
}
+// Issue #1408: SelectiveChannel::AddChannel must take over (or not take over)
+// the sub channel consistently w.r.t. option.ownership, even on failure, so
+// that callers don't leak or double-free.
+class CountingSubChannel : public brpc::Channel {
+public:
+ static int s_alive;
+ CountingSubChannel() { ++s_alive; }
+ ~CountingSubChannel() override { --s_alive; }
+};
+int CountingSubChannel::s_alive = 0;
+
+TEST_F(ChannelTest, selective_channel_add_failure_ownership) {
+ // nullptr sub channel is rejected without touching ownership.
+ {
+ brpc::SelectiveChannel channel;
+ ASSERT_EQ(0, channel.Init("rr", nullptr));
+ EXPECT_EQ(-1, channel.AddChannel(nullptr, nullptr));
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+
+ // AddChannel before Init() honors the contract as well: an OWNS_CHANNEL
+ // sub channel is consumed, a DOESNT_OWN_CHANNEL one is left to the caller.
+ {
+ brpc::SelectiveChannel channel; // no Init()
+ CountingSubChannel* u = new CountingSubChannel();
+ ASSERT_EQ(0, u->Init("127.0.0.1:1", nullptr));
+ ASSERT_EQ(1, CountingSubChannel::s_alive);
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::OWNS_CHANNEL;
+ EXPECT_EQ(-1, channel.AddChannel(u, opt, nullptr));
+ EXPECT_EQ(0, CountingSubChannel::s_alive);
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+ {
+ brpc::SelectiveChannel channel; // no Init()
+ CountingSubChannel* u = new CountingSubChannel();
+ ASSERT_EQ(0, u->Init("127.0.0.1:1", nullptr));
+ ASSERT_EQ(1, CountingSubChannel::s_alive);
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::DOESNT_OWN_CHANNEL;
+ EXPECT_EQ(-1, channel.AddChannel(u, opt, nullptr));
+ EXPECT_EQ(1, CountingSubChannel::s_alive);
+ delete u;
+ EXPECT_EQ(0, CountingSubChannel::s_alive);
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+
+ // Plan 1: a duplicate AddChannel never reassigns ownership; the FIRST
+ // successful registration is the single source of truth.
+ // First reg = OWNS, duplicate with DOESNT_OWN must NOT downgrade: schan
+ // still owns and deletes the channel on removal; caller must NOT delete.
+ {
+ brpc::SelectiveChannel channel;
+ ASSERT_EQ(0, channel.Init("rr", nullptr));
+ brpc::SelectiveChannel::ChannelHandle h;
+ CountingSubChannel* c = new CountingSubChannel();
+ ASSERT_EQ(0, c->Init("127.0.0.1:1", nullptr));
+ ASSERT_EQ(1, CountingSubChannel::s_alive);
+ {
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::OWNS_CHANNEL;
+ ASSERT_EQ(0, channel.AddChannel(c, opt, &h));
+ }
+ {
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::DOESNT_OWN_CHANNEL;
+ EXPECT_EQ(-1, channel.AddChannel(c, opt, nullptr));
+ }
+ // Ownership unchanged (still OWNS): schan deletes c.
+ channel.RemoveAndDestroyChannel(h);
+ EXPECT_EQ(0, CountingSubChannel::s_alive);
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+
+ // First reg = DOESNT_OWN, duplicate with OWNS must NOT upgrade: the caller
+ // still owns the channel and must delete it; schan does not.
+ {
+ brpc::SelectiveChannel channel;
+ ASSERT_EQ(0, channel.Init("rr", nullptr));
+ brpc::SelectiveChannel::ChannelHandle h;
+ CountingSubChannel* d = new CountingSubChannel();
+ ASSERT_EQ(0, d->Init("127.0.0.1:1", nullptr));
+ ASSERT_EQ(1, CountingSubChannel::s_alive);
+ {
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::DOESNT_OWN_CHANNEL;
+ ASSERT_EQ(0, channel.AddChannel(d, opt, &h));
+ }
+ {
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::OWNS_CHANNEL;
+ EXPECT_EQ(-1, channel.AddChannel(d, opt, nullptr));
+ }
+ channel.RemoveAndDestroyChannel(h);
+ // Ownership unchanged (still DOESNT_OWN): schan does not delete d.
+ EXPECT_EQ(1, CountingSubChannel::s_alive);
+ delete d;
+ EXPECT_EQ(0, CountingSubChannel::s_alive);
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+
+ // OWNS_CHANNEL on success: schan takes over and deletes on removal.
+ {
+ brpc::SelectiveChannel channel;
+ ASSERT_EQ(0, channel.Init("rr", nullptr));
+ brpc::SelectiveChannel::ChannelHandle h;
+ CountingSubChannel* e = new CountingSubChannel();
+ ASSERT_EQ(0, e->Init("127.0.0.1:1", nullptr));
+ ASSERT_EQ(1, CountingSubChannel::s_alive);
+ {
+ brpc::SelectiveChannel::SubChannelOptions opt;
+ opt.ownership = brpc::OWNS_CHANNEL;
+ ASSERT_EQ(0, channel.AddChannel(e, opt, &h));
+ }
+ channel.RemoveAndDestroyChannel(h);
+ EXPECT_EQ(0, CountingSubChannel::s_alive);
+ }
+ ASSERT_EQ(0, CountingSubChannel::s_alive);
+}
+
} //namespace
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]