Skip to content

fix(future): race-safe remote_future_dispose — sever producer under the mutex before closing the trigger (true-async/server#93)#182

Merged
EdmondDantes merged 1 commit into
mainfrom
fix-remote-future-dispose-race
Jul 7, 2026
Merged

fix(future): race-safe remote_future_dispose — sever producer under the mutex before closing the trigger (true-async/server#93)#182
EdmondDantes merged 1 commit into
mainfrom
fix-remote-future-dispose-race

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Problem

remote_future_dispose() closed the shared-state's cross-thread trigger and dropped the state ref without first marking the state completed or detaching target_future. A producer thread completing the future at the same moment (async_future_shared_state_complete/reject) could then:

  • ring a trigger that is being uv_closed, or
  • resolve a target_future that the owner just freed.

So disposing an unresolved remote future was unsafe — only safe once it had already resolved.

Fix

Mirror async_future_state_object_free(): under the shared-state mutex, grab the trigger, NULL it, set completed = 1 and clear target_future, then dispose the trigger outside the lock. A concurrent complete()/reject() (both gated on completed under the same mutex) becomes a clean no-op.

This lets an owner dispose an unresolved remote future safely — e.g. HttpServer reaping the per-worker completion futures returned by submit_internal() on pool shutdown (true-async/server#93), which otherwise left their triggers armed on the parent reactor (loop-alive assert on debug / leaked libuv handle on release).

Test

Covered end-to-end by the server suite (graceful_shutdown + reload → shutdown); no ABI change.

…er the shared-state mutex before closing the trigger (true-async/server#93)

Disposing a remote future while its producer thread is still completing it raced:
remote_future_dispose closed the cross-thread trigger and dropped the state ref
without first marking the state completed or detaching target_future, so a
concurrent async_future_shared_state_complete() could ring a closing handle or
resolve a freed future. It now mirrors async_future_state_object_free — under the
state mutex it grabs the trigger, NULLs it, sets completed=1 and clears
target_future, then disposes the trigger outside the lock — so a late completion
is a clean no-op. This lets an owner dispose an unresolved remote future safely
(e.g. the HTTP server reaping per-worker completion futures on pool shutdown).
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes EdmondDantes merged commit e3ad62a into main Jul 7, 2026
9 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