Skip to content

Combine cursor bot bug fixes (bridge/RPC lifetimes, Asio waits, address validation, shadow recovery)#109

Merged
dallison merged 8 commits into
mainfrom
combined-cursor-bugfixes
Jul 16, 2026
Merged

Combine cursor bot bug fixes (bridge/RPC lifetimes, Asio waits, address validation, shadow recovery)#109
dallison merged 8 commits into
mainfrom
combined-cursor-bugfixes

Conversation

@dallison

@dallison dallison commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

The cursor bug-investigation automation opened a number of PRs against
main that cannot be merged directly (bot authorship / CLA). This PR
re-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 main and each
commit is scoped to one issue.

Included fixes

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 wrong
    index), 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 shared
    buffer. 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_arm64
  • bazelisk test //... --config=macos_arm64 (25 tests pass)
  • Split-buffer client tests: //client:client_test //c_client:client_test --test_arg=--use_split_buffers
  • Bridge tests with and without split buffers
  • Asio backend: //common/async:async_test //common:split_buffer_test //client:client_test //client:bridge_test //server:server_test --//:coro_backend=asio
  • Asio bridge retirement filters (BasicRetirement, MultipleRetirement, MultipleRetirement2)

Not runnable on macOS: the Linux --config=linux_memfd matrix. The
excluded #103 is the only dropped change that touched the memfd buffer
path; the included changes do not.

dallison added 8 commits July 6, 2026 17:43
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.
@dallison
dallison merged commit c4f2745 into main Jul 16, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant