mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
fix(protocol): the drain barrier must not dangle on its own timeout
Two defects in the barrier added by 1e9c934, both on the path it takes when
it fails:
std::promise<void> drained; // stack local
boost::asio::post(ioc, [&drained] { drained.set_value(); }); // by REFERENCE
fut.wait_for(std::chrono::seconds(5)); // result dropped
1. The wait is bounded, so on timeout this frame returns while the posted task
is still queued -- and the task holds a pointer to a destroyed stack object.
set_value() then writes to freed stack memory. The bound that stops a wedged
I/O thread hanging teardown introduced the exact class of use-after-free the
barrier exists to prevent. The promise is now a shared_ptr captured BY VALUE,
so the task keeps it alive whether or not anyone is still waiting.
2. The wait_for result was discarded. A timeout means the barrier did NOT hold
and we are about to free an IncomingCallHandler that a live connection may
still call back into -- the original crash, minus any way to know it
happened. It now warns, naming the consequence.
Neither is reachable while the I/O thread drains promptly, which is why the
suite is green either way; both matter precisely when it does not, which is
the only situation the barrier is for.
Tests: 270/270.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e9c93434b
commit
4f9d824129
@@ -231,10 +231,23 @@ PlainTransportHost::~PlainTransportHost()
|
||||
if (tcp || ssl) {
|
||||
auto& ioc = IoContextPool::shared().ioContext();
|
||||
if (!ioc.get_executor().running_in_this_thread()) {
|
||||
std::promise<void> drained;
|
||||
auto fut = drained.get_future();
|
||||
boost::asio::post(ioc, [&drained] { drained.set_value(); });
|
||||
fut.wait_for(std::chrono::seconds(5)); // safety-bounded; work-guard keeps the thread alive
|
||||
// The promise is shared, NOT captured by reference. The wait below is
|
||||
// bounded, so on timeout this frame returns while the posted task is
|
||||
// still queued — a by-reference capture would then set_value() on a
|
||||
// destroyed stack object, which is the very failure mode this barrier
|
||||
// exists to prevent.
|
||||
auto drained = std::make_shared<std::promise<void>>();
|
||||
auto fut = drained->get_future();
|
||||
boost::asio::post(ioc, [drained] { drained->set_value(); });
|
||||
// Bounded so a wedged I/O thread cannot hang teardown — but a timeout
|
||||
// means the barrier did NOT hold and we are about to free an
|
||||
// IncomingCallHandler a connection may still call back into. Say so:
|
||||
// silently proceeding is how this class of crash stays unexplained.
|
||||
if (fut.wait_for(std::chrono::seconds(5)) != std::future_status::ready) {
|
||||
qWarning() << "PlainTransportHost: I/O drain timed out after 5s;"
|
||||
<< "tearing down anyway — a connection callback may still"
|
||||
<< "reference this host (see the barrier comment above)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user