From 3d03f12a3b6a3a1e9469ba6f1ee17b3ba6aad4f7 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:47:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(future):=20race-safe=20remote=5Ffuture=5Fdi?= =?UTF-8?q?spose=20=E2=80=94=20sever=20the=20producer=20under=20the=20shar?= =?UTF-8?q?ed-state=20mutex=20before=20closing=20the=20trigger=20(true-asy?= =?UTF-8?q?nc/server#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- future.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/future.c b/future.c index e9541a23..a328a14e 100644 --- a/future.c +++ b/future.c @@ -2216,10 +2216,23 @@ static bool remote_future_dispose(zend_async_event_t *event) zend_future_t *future = &remote->future; if (remote->state != NULL) { + /* Sever the producer under the mutex before closing the trigger, so a + * concurrent complete() becomes a no-op (mirrors state_object_free). */ + zend_async_trigger_event_t *trigger_to_dispose = NULL; + + ASYNC_MUTEX_LOCK(remote->state->mutex); if (remote->state->trigger != NULL) { - remote->state->trigger->base.dispose(&remote->state->trigger->base); + trigger_to_dispose = remote->state->trigger; remote->state->trigger = NULL; + zend_atomic_int_store(&remote->state->completed, 1); + } + remote->state->target_future = NULL; + ASYNC_MUTEX_UNLOCK(remote->state->mutex); + + if (trigger_to_dispose != NULL) { + trigger_to_dispose->base.dispose(&trigger_to_dispose->base); } + async_future_shared_state_delref(remote->state); remote->state = NULL; }