Skip to content

#161: carry closure scope/called_scope by name across thread transfer#163

Merged
EdmondDantes merged 3 commits into
mainfrom
161-static-closure-scope-transfer
Jun 12, 2026
Merged

#161: carry closure scope/called_scope by name across thread transfer#163
EdmondDantes merged 3 commits into
mainfrom
161-static-closure-scope-transfer

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Fixes the scope half of #161.

A transferred closure lost its class scope: a static closure declared inside a class arrived in the worker unscoped, so its first self::/static:: threw Cannot access "self" when no class scope is active — and the worker died silently. A $this-bound closure got Z_OBJCE($this) instead of its declaring class, breaking self:: and private-member visibility under inheritance.

Changes

  • Snapshot stores scope/called_scope as persistent name strings; async_thread_create_closure re-resolves them in the target thread via zend_lookup_class (autoload-aware).
  • Missing class is a hard error: Cannot restore closure scope: class "X" not found in the target thread — no silently unscoped closure.
  • Closures scoped to anonymous classes are rejected at transfer time.
  • Bootloaders (spawn_thread slot and pool bootloader) are deliberately copied unscoped: they bootstrap a thread where their own declaring class may not exist yet.
  • thread_call_closure passes called_scope as the frame's object_or_scope for scoped static calls and skips execution when creation threw.
  • closure_transfer_obj LOAD failure follows the default-loader convention: throw + stdClass fallback.

Tests

  • tests/thread/072 — issue repro: static closure with self::/static:: through spawn_thread.
  • tests/thread/073 — scope class missing in worker → clear error.
  • tests/thread/057 — re-pinned to native scope semantics (worker: base), as its description anticipated.

The deadlock half of #161 (parent parked forever on a ThreadChannel after silent worker death) is a separate problem shared with #162 and is not addressed here.

A transferred closure lost its class scope: a static closure declared
inside a class arrived in the worker unscoped (first self::/static::
threw "Cannot access self"), and a $this-bound closure got Z_OBJCE($this)
instead of its declaring class, breaking self:: and private visibility
under inheritance.

The snapshot now stores scope and called_scope as persistent name
strings; async_thread_create_closure re-resolves them in the target
thread via zend_lookup_class (autoload-aware). A missing class is a hard
error instead of a silently unscoped closure. Closures scoped to
anonymous classes are rejected at transfer time.

Bootloaders are deliberately copied unscoped: they bootstrap a thread
where their own declaring class may not exist yet (chicken-and-egg).
Applies to both the spawn_thread bootloader slot and the pool bootloader
stored in the entry slot (entry_is_bootloader).

thread_call_closure now passes called_scope as the frame's
object_or_scope for scoped static calls (zend_vm_init_call_frame
asserts otherwise) and skips execution when closure creation threw.
The closure_transfer_obj LOAD branch follows the default-loader
convention on failure: throw + stdClass fallback.
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Worker payloads are lexically declared inside StandardSteps, but chaos
workers boot bare (no harness classes). With strict scope transfer the
class would now be required on the worker side; strip the scope via
Closure::bind(fn, null, null) — the payloads never touch self::/static::.
Same contract as the spawn_thread payloads in the previous commit: pool
workers boot bare, so harness-scoped task closures must be stripped of
their lexical class scope before transfer.
@EdmondDantes EdmondDantes merged commit abd0d38 into main Jun 12, 2026
9 checks passed
@EdmondDantes EdmondDantes deleted the 161-static-closure-scope-transfer branch June 12, 2026 09:37
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