Skip to content

fix: preserve a closure's bound $this across thread transfer#123

Merged
EdmondDantes merged 1 commit into
mainfrom
fix/closure-transfer-preserve-bound-this
May 21, 2026
Merged

fix: preserve a closure's bound $this across thread transfer#123
EdmondDantes merged 1 commit into
mainfrom
fix/closure-transfer-preserve-bound-this

Conversation

@EdmondDantes
Copy link
Copy Markdown
Contributor

Bug

A $this-bound closure transferred to another thread through the generic closure-transfer path arrives unbound. Invoking a method- or object-bound closure in the destination thread then dereferences a NULL $this — SIGSEGV on the first $this access.

Reproduced end-to-end: an object-bound HTTP handler registered on TrueAsync\HttpServer with setWorkers(N>1) segfaults every worker on the first request. A direct Async\ThreadPool submit of an object-bound closure is not affected — the difference led straight to the cause.

Cause

closure_transfer_obj() (the transfer_obj handler installed on Closure) builds the transfer snapshot like this:

zend_fcall_t fcall;
memset(&fcall, 0, sizeof(fcall));
fcall.fci_cache.function_handler = (zend_function *) func;
async_thread_snapshot_t *snapshot = async_thread_snapshot_create(&fcall, NULL);

Only fci_cache.function_handler is set. But thread_copy_callable() reads the bound instance from fci_cache.object:

zend_object *bound_obj = fcall->fci_cache.object;   /* NULL here */
...
if (bound_obj != NULL) { /* transfer bound_this */ }   /* skipped */

So bound_this stays UNDEF, and async_thread_create_closure() recreates an unbound closure.

spawn_thread / ThreadPool tasks are unaffected because they hand the snapshot a zend_fcall_t whose fci_cache.object is already resolved. The defect is specific to closure_transfer_obj — the path taken when a closure travels as a bound variable, array element, argument, or channel message (or via an extension's ZEND_ASYNC_THREAD_TRANSFER_ZVAL).

Fix

Populate fci_cache.object from the closure's this_ptr before snapshotting. The snapshot/recreate machinery already handles bound_this end to end once it is given the instance — no other change needed.

Test

tests/thread/058-spawn_thread_captured_bound_closure.phpt — a $this-bound closure captured as a bound variable of a spawn_thread task (forcing the generic path), invoked in the worker; also asserts the binding is a deep copy. Fails (NULL-$this deref) before the fix, passes after.

closure_transfer_obj() built the transfer snapshot from a zend_fcall_t
with only fci_cache.function_handler set. thread_copy_callable() takes the
bound instance from fci_cache.object, so leaving it NULL snapshotted the
closure unbound: the destination thread recreated an object-less closure,
and a method/object-bound closure then dereferenced a NULL $this — SIGSEGV
on the first $this access.

Populate fci_cache.object from the closure's this_ptr before snapshotting;
the snapshot/recreate machinery already handles bound_this once given the
instance.

Affects the generic closure-transfer path only — a $this-bound closure
travelling as a bound variable, array element, argument or channel
message. spawn_thread / ThreadPool tasks were unaffected: they resolve
fci_cache.object themselves.

Adds tests/thread/058-spawn_thread_captured_bound_closure.phpt.
EdmondDantes added a commit to true-async/server that referenced this pull request May 21, 2026
The regression test depends on true-async/php-async#123; until that fix
is in the CI PHP build a $this-bound handler closure loses its binding
across the worker transfer and the test fails. XFAIL keeps CI green in
both states (expected-fail before the fix, pass after); the section is
to be removed once php-async#123 is merged.
@EdmondDantes EdmondDantes merged commit db3cb9d into main May 21, 2026
6 checks passed
@EdmondDantes EdmondDantes deleted the fix/closure-transfer-preserve-bound-this branch May 21, 2026 08:04
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