#3657·seastar

rpc: make_stream_sink() can outlive the client that is creating the stream

Author: bhalevyCreated Aug 31, 2026Updated Aug 31, 2026

client::make_stream_sink() builds the stream connection across two asynchronous steps, and captures the parent client by raw this for the second one:

cpp
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:

  1. the application calls make_stream_sink();
  2. while the new stream connection is negotiating, the owner calls client::stop() on the parent — which completes, because the stream is not registered in _streams yet and there is nothing else for stop() to wait for — and then destroys the parent;
  3. c->await_connection() resolves and the continuation runs, reading _error and calling register_stream() through a dangling this.

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.