Skip to content

fix(reflection): skip closures over internal functions when walking the live closures - #290

Merged
lisachenko merged 1 commit into
8.4from
fix/live-closures-over-internal-functions
Oct 10, 2026
Merged

lisachenko merged 1 commit into
8.4from
fix/live-closures-over-internal-functions

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

What this changes

A body swap walks the live closure objects to find the copies of its entry (fake closures share the entry's run-time cache and borrow its live statics; see FunctionBodySwap::liveClosureFunctions()). The walk built a user-function view over every closure and only then asked whether it was user-defined. A closure over an internal function (a first-class callable strlen(...), Closure::fromCallable('strlen'), the closure of an internal method) keeps its name in the internal_function member, so building the view threw FFI\Exception: Attempt to read undefined field 'function_name' first. Every redefine() that released a minted run-time cache, let a preserveAs alias take over live statics, or retired a removed alias failed while such a closure was alive anywhere in the process.

Found through the cascade of 8.5 into master (#285/#289): PHP 8.6 keeps closures over internal functions in its callable caches, so there every one of those paths failed (14 errors). Reproduced on PHP 8.4 with a strlen(...) kept alive across two swaps. It exists on every branch since the feature landed, so the fix targets 8.4 and cascades.

The function type is now read off the zend_function union before any view is made, with the same ZEND_USER_FUNCTION test FunctionLikeTrait::isUserDefined() uses, and internal closures are skipped. RedefineLiveInternalClosureTest holds the three kinds of internal closure alive through the cache release, the alias take-over of live statics and removeMethods(), in both runner modes.

The same hunk is ported into #289 so the 8.6 cascade can go green without waiting for this one; it no-ops once the cascade carries it.

Environment it was verified on

  • PHP version (full first line of php -v): PHP 8.4.19 (cli) (built: Oct 10 2026) (NTS)
  • Thread safety: NTS
  • OS / architecture: Linux x86_64
  • Debug build (--enable-debug)? no

Verified: the new test in both runner modes, composer test (minus the pre-existing ExecutionDataTest::testGetThis abort of this build), composer phpstan, composer cs:check.

Checklist

  • Targets the minimum affected version branch (master = newest
    supported minor, 8.4 = PHP 8.4, …) — fixes cascade upward, never
    downward
  • composer test passes on the matching PHP minor
  • composer phpstan (level max) and composer cs:check are green
  • Tests added or updated; structs the code dereferences are listed in
    layout_structs in tools/generator/symbols.php (nothing new)
  • If tools/generator/symbols.php changed: regenerated with
    composer gen-headers and committed the result — nothing under
    include/, stubs/ or .phpstorm.meta.php was hand-edited (unchanged)
  • Conventional Commits used for the commit messages

🤖 Generated with Claude Code

https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2


Generated by Claude Code

…he live closures

A swap walks the live closure objects to find the copies of its entry (fake
closures share the entry's run-time cache and borrow its live statics). The
walk built a user-function view over every closure and asked it whether it
was user-defined - but a closure over an INTERNAL function (a first-class
callable of strlen(), Closure::fromCallable() over an internal function, the
closure of an internal method) keeps its name in the internal_function
member, and building the view threw "Attempt to read undefined field
'function_name'" before the question could be asked. Every redefine() that
released a minted cache, took over live statics or retired an alias failed
while such a closure was alive; PHP 8.6 keeps such closures in its callable
caches, so there it failed always.

The function type is now read off the union before any view is made, and
internal closures are skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
@lisachenko
lisachenko marked this pull request as ready for review October 10, 2026 17:51
@lisachenko
lisachenko merged commit fccd4cf into 8.4 Oct 10, 2026
21 checks passed
@lisachenko
lisachenko deleted the fix/live-closures-over-internal-functions branch October 10, 2026 17:51
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.

2 participants