rpc: make_stream_sink() can outlive the client that is creating the stream
client::make_stream_sink() builds the stream connection across two asynchronous steps, and captures the parent client by raw this for the second one:
future<sink<Out...>> make_stream_sink(socket socket) {
return await_connection().then([this, socket = std::move(socket)] () mutable {
...
auto c = make_shared<client>(_logger, _serializer, o, std::move(socket), _server_addr, _local_addr);
c->_parent = this->weak_from_this();
c->_is_stream = true;
return c->await_connection().then([c, this] {
if (_error) {
throw closed_error();
}
xshard_connection_ptr s = make_lw_shared(make_foreign(static_pointer_cast<rpc::connection>(c)));
this->register_stream(c->get_connection_id(), s);
...Nothing keeps the parent alive across either then(). So this ordering is possible:
- the application calls
make_stream_sink(); - while the new stream connection is negotiating, the owner calls
client::stop()on the parent — which completes, because the stream is not registered in_streamsyet and there is nothing else forstop()to wait for — and then destroys the parent; c->await_connection()resolves and the continuation runs, reading_errorand callingregister_stream()through a danglingthis.
The same hole exists for the outer continuation, which uses this after await_connection() on the parent itself.
This is not what #3653 was about — there the stream connection was fully created and registered — but it is the same class of problem: an owner can go away while something still refers to it. It also means the invariant PR #3655 establishes ("a client owns its stream connections for as long as their loops are running, and client::stop() waits for them") does not extend to streams that are still coming up.
The fix suggested during review of #3655 is to take a holder on the parent's _streams_gate at the start of make_stream_sink(), before any await, rather than only once the connection is registered. Then client::stop() also waits out in-flight stream creation, and a make_stream_sink() that starts on an already-stopping client fails cleanly instead of touching a destroyed parent.
Source: scylladb/seastar