Combine cursor bot bug fixes (bridge/RPC lifetimes, Asio waits, address validation, shadow recovery)#109
Merged
Merged
Conversation
Streaming RPC handlers in the rpc, asio_rpc, coro_rpc and co20_rpc server backends spawned a detached cancel watcher that captured the per-request AnyStreamWriter and RpcRequest stack objects by reference. After a stream completed normally or errored, a later cancel/shutdown wakeup could resume the watcher and access freed stack memory, risking server crashes or request-id corruption. AnyStreamWriter also stored the RpcRequest by reference, extending the dangling-reference hazard. Make AnyStreamWriter own a copy of the RpcRequest and share an atomic cancellation state, have watchers capture only stable request/session IDs plus the shared state and a per-stream stop pipe, and wake each watcher when the handler returns so normal completion leaves no suspended watcher behind. Combines cursor bot PR #90.
Two problems in the optional Boost.Asio coroutine backend: - async::WaitEither had no timeout support, so client wrappers such as Subscriber::Wait(fd, timeout, ctx) and reliable publisher waits discarded the caller-provided timeout and could hang forever when neither fd became ready. Added timeout support to WaitEither (with a non-suspending fast path) and passed the client deadlines through. - Asio wait/socket helpers unconditionally installed cancellation handlers on the yield_context cancellation slot. Plain boost::asio::spawn(..., detached) coroutines have no connected cancellation slot, so ordinary async pub/sub paths could abort inside Boost.Asio. Guard cancellation-slot assignment/clear with is_connected(). Combines cursor bot PR #92 (supersedes the cancellation-slot portion of PR #99).
Bridged reliable channels with retirement notifications could crash or wedge when retirement messages raced with bridge teardown or arrived immediately after a bridged send: - The retirement receiver was spawned (via SpawnOnNewStrand, which posts work asynchronously) with a reference to a parent coroutine's stack-owned listener socket, and the retirement notifier captured a stack-owned transmitter. Shutdown or early bridge teardown could leave spawned coroutines using destroyed sockets. - Outstanding bridged ActiveMessage tracking was updated only after the network send and shared without synchronization, so a fast peer could retire a slot before it was recorded, causing a null dereference (the old code did (*active_retirement_msgs)[slot_id]->DecRef() with no null check) or a permanently pinned reliable slot. Move the retirement listener/transmitter sockets to shared ownership and close them when parent bridge coroutines unwind so helpers wake safely. Add a synchronized BridgeRetirementState that tracks bridged ActiveMessages before sending, releases duplicate/late/failed retirements safely, and cleans up outstanding slots on teardown. Combines cursor bot PR #107 (supersedes duplicate PRs #100, #102, #106 and the bridge portions of #93 and #99).
Malformed discovery/bridge protobufs could set a channel address family while providing fewer than four address bytes. The server decoded those bytes with unchecked memcpy/reinterpret_cast in bridge subscribe and retirement handling, letting a network peer trigger out-of-bounds reads/undefined behavior and potential server crashes. Add a shared ParseChannelAddress helper that validates address length and family before decoding, and use it for both the subscribe receiver and retirement socket parsing. Combines cursor bot PR #97.
When a server recovered channel state from a shadow, RecoverFromShadow re-created the subscriber users but never rebuilt the recovered channel's CCB subscriber registrations, so a channel could come back with a stale or zero subscriber count until the next remap. Call RegisterExistingSubscribers() after re-adding the recovered subscribers, matching the RemapChannel path. Combines cursor bot PR #96.
The BridgeRetirementState vector was shared between the bridge transmitter and its retirement-reader coroutine, which ran on two separate strands, so it needed a mutex to stay correct under a multi-threaded io_context. Spawn the retirement reader on the transmitter's own strand (new AsyncRuntime::SpawnOnCurrentStrand) so the two are serialized by the strand and the mutex can be removed. They still interleave cooperatively at every suspension point, so the reader keeps draining retirements while the transmitter is parked on backpressure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The cursor bug-investigation automation opened a number of PRs against
mainthat cannot be merged directly (bot authorship / CLA). This PRre-authors the ones that are still valid and represent real bugs into a
single set of commits, de-duplicating the many overlapping variants the
automation produced.
Each fix was verified to still reproduce against current
mainand eachcommit is scoped to one issue.
Included fixes
Fix streaming RPC cancel watcher lifetimes (from Fix streaming RPC cancel watcher lifetimes #90; supersedes
Fix streaming RPC cancel watcher lifetime #101 and the RPC portion of Fix RPC cancellation and bridge listener lifetimes #93) —
AnyStreamWriterheld theRpcRequestby reference and detached cancel watchers capturedper-request stack state by reference, so a late cancel/shutdown wakeup
could touch freed memory. Now the writer owns the request, cancellation
state is a shared atomic, watchers capture only stable IDs + a
per-stream stop pipe, and each watcher is woken when the handler
returns. Applied consistently across
rpc,asio_rpc,coro_rpc,co20_rpc.Fix Asio wait timeouts and cancellation-slot crashes (from Fix Asio wait hangs and cancellation-slot crashes #92;
supersedes the cancellation-slot portion of Fix Asio bridge retirement and cancellation-slot crashes #99) —
async::WaitEitherignored caller timeouts (potential permanent hangs) and the Asio
wait/socket helpers assumed every
yield_contexthad a connectedcancellation slot, aborting for plain
boost::asio::spawn(..., detached)coroutines. Added timeout support + a non-suspending fastpath and guarded cancellation-slot access with
is_connected().Fix bridge retirement lifetime races (from Fix bridge retirement lifetime races #107; supersedes Fix bridge retirement socket lifetimes #100,
Fix bridge retirement socket lifetimes #102, Fix bridge retirement coroutine lifetimes #106 and the bridge portion of Fix RPC cancellation and bridge listener lifetimes #93/Fix Asio bridge retirement and cancellation-slot crashes #99) — retirement helper
coroutines captured stack-owned listener/transmitter sockets by
reference even though
SpawnOnNewStrandposts work asynchronously, andoutgoing
ActiveMessages were tracked only after the network sendwithout synchronization (a fast peer could retire a slot early, causing
a null deref or a permanently pinned slot). Sockets now use shared
ownership with close-on-teardown guards, and a mutex-guarded
BridgeRetirementStatetracks messages before sending and releasesduplicate/late/failed retirements safely.
Validate bridged channel address payloads (from Validate bridged channel address payloads #97) — malformed
discovery/bridge protobufs could declare an address family with fewer
than four address bytes, which the server decoded with unchecked
memcpy/reinterpret_cast, enabling out-of-bounds reads. Added ashared
ParseChannelAddresshelper that validates length/family beforedecoding.
Rebuild CCB subscriber registrations on shadow recovery (from Fix shadow recovery subscriber registrations #96)
—
RecoverFromShadowrecreated subscriber users but never rebuilt therecovered channel's CCB subscriber registrations, so a channel could
come back with a stale/zero subscriber count. Added the
RegisterExistingSubscribers()call, matching theRemapChannelpath.Deliberately excluded
Fix publisher attach corrupting resized buffer metadata #103 (publisher attach resized-buffer metadata) — surfaces a real
latent off-by-one (
bcb_->sizes[buffers_.size()]writes the wrongindex), but its own regression test does not pass on the POSIX shm
backend (macOS / default when not memfd), which CI exercises. The
automation appears to have validated only the Linux/memfd path. The
fix does not resolve the reported symptom on POSIX and would turn CI
red, so it needs a proper platform-agnostic fix rather than a straight
combine.
Validate client buffer registration ownership #83 (client buffer registration ownership) — its single-owner
enforcement predates the anonymous-memfd shared-buffer model
introduced in Allow memfd_create shared-memory backend on Linux, independent of Android #85, where multiple publishers legitimately register the
same
(session, buffer_index, slot)group and converge on one sharedbuffer. As written it would reject those registrations and regress
Android/memfd split buffers, so it is no longer valid without a
redesign.
The duplicate bridge/RPC/Asio investigation PRs listed above as
"supersedes" are covered by the chosen, most complete variant.
Test plan
All run locally on macOS arm64 (mirrors the CI steps):
bazelisk build //... --config=macos_arm64bazelisk test //... --config=macos_arm64(25 tests pass)//client:client_test //c_client:client_test --test_arg=--use_split_buffers//common/async:async_test //common:split_buffer_test //client:client_test //client:bridge_test //server:server_test --//:coro_backend=asioBasicRetirement,MultipleRetirement,MultipleRetirement2)Not runnable on macOS: the Linux
--config=linux_memfdmatrix. Theexcluded #103 is the only dropped change that touched the memfd buffer
path; the included changes do not.