Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions lib/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -1689,8 +1689,13 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame {
$return->closureCall = $frame->closureCall;
}
// Bound Closure::bind/bindTo scope must survive ?? / if / try CFG edges (#24335).
// Methods fall back to func->class; top-level bound closures only have calledClass.
if (null !== $frame->calledClass && '' !== $frame->calledClass) {
// Copy only on same-function CFG continuations — nested calls must not inherit the
// caller's LSB (instance-method FCC would poison later static:: / : static, #32083).
if (
$this->isSameFunctionCfgEdge($frame)
&& null !== $frame->calledClass
&& '' !== $frame->calledClass
) {
$return->calledClass = $frame->calledClass;
}
if (null !== $frame->scopeClass && '' !== $frame->scopeClass) {
Expand Down Expand Up @@ -1722,6 +1727,21 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame {
return $return;
}

/**
* Same user function / {main} CFG continuation — not a nested call (#24335 vs #32083).
*
* php-src: called_scope is per execute_data; ZEND_AST_CALLABLE_CONVERT must not change
* later methods' late-static scope (Zend/zend_execute.c).
*/
private function isSameFunctionCfgEdge(Frame $frame): bool
{
if (null === $frame->block || null === $this->func || null === $frame->block->func) {
return false;
}

return $this->func === $frame->block->func;
}

/**
* Opcodes may reference slot indices without a matching scope operand (#5911, enum ctor assign).
*/
Expand Down
17 changes: 5 additions & 12 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -19000,12 +19000,11 @@ protected function bindClosureCallCaptures(Frame $callee, ?ClosureState $closure
protected function initClosureCall(Frame $frame, ClosureState $state): void
{
if (null !== $state->methodName && null !== $state->methodReceiver) {
$calledScope = $this->closureCalledScopeClass($state);
if (null !== $calledScope && '' !== $calledScope) {
$frame->calledClass = $calledScope;
}
// Callee LSB is applied in applyClosureBinding — do not write the FCC class onto
// the caller frame (that poisoned later static:: / : static in the unit, #32083).
$this->initMethodCall($frame, $state->methodReceiver, $state->methodName);
$frame->closureCall = null;
$frame->pendingClosureInvoke = $state;

return;
}
Expand All @@ -19016,26 +19015,20 @@ protected function initClosureCall(Frame $frame, ClosureState $state): void
&& null !== $state->wrappedFunc
&& null === $state->methodReceiver
) {
$calledScope = $this->closureCalledScopeClass($state);
if (null !== $calledScope && '' !== $calledScope) {
$frame->calledClass = $calledScope;
}
$frame->magicCallMethodName = $state->methodName;
$frame->call = $state->wrappedFunc;
$frame->closureCall = null;
$frame->pendingClosureInvoke = $state;
$frame->callArgs = [];
$frame->callArgEntries = [];
$frame->builtinCalleeQualifiedMethod = null;

return;
}
if (null !== $state->wrappedFunc) {
$calledScope = $this->closureCalledScopeClass($state);
if (null !== $calledScope && '' !== $calledScope) {
$frame->calledClass = $calledScope;
}
$frame->call = $state->wrappedFunc;
$frame->closureCall = null;
$frame->pendingClosureInvoke = $state;
// Scoped parent/self FCC (#17655/#26630) and fromCallable instance wrappers clear
// methodReceiver and call wrappedFunc directly. Instance methods still need $this
// as callArgs[0] so user args land at ARG_RECV indices 1..n (#27834).
Expand Down
2 changes: 1 addition & 1 deletion test/compliance/ClosureVMTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function setUp(): void

public static function providePHPTests(): \Generator
{
foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'arrow_fn_byref.phpt', 'arrow_fn_this_outside_context.phpt', 'nested_arrow_function.phpt', 'nested_arrow_capture_no_undefined_warning.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_recursive_use_byref.phpt', 'closure_this_binding.phpt', 'closure_auto_bind_private.phpt', 'closure_parent_method_private_scope.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_from_callable_this_private_invoke.phpt', 'closure_from_callable_class_in_closure_private.phpt', 'closure_from_callable_inaccessible.phpt', 'closure_from_callable_static_private_binds_this.phpt', 'closure_fromcallable_static_scope_typeerror.phpt', 'closure_from_callable_enum_case.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_inline_new_private.phpt', 'closure_bind_instance_inline_new.phpt', 'closure_bindto_inline_new_object.phpt', 'closure_bind_enum_case.phpt', 'closure_bindto_enum_case.phpt', 'closure_bindto_enum_scope.phpt', 'closure_bindto_null_class_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'closure_bindto_illegal_returns_null.phpt', 'closure_bindto_null_free_uses_this.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt', 'static_closure_bind.phpt', 'closure_call_static_warns.phpt', 'static_call_unbound_closure.phpt', 'closure_static_var.phpt', 'closure_static_counter.phpt', 'closure_static_use_byref.phpt', 'static_var_closure_init_fatal.phpt', 'static_var_arrow_init_fatal.phpt', 'inline_static_closure_call_arg.phpt', 'closure_call_method_rebind.phpt', 'fcc_default_parameter_compile_error.phpt', 'fcc_instance_method.phpt', 'fcc_instance_expr.phpt', 'fcc_instance_method_error.phpt', 'fcc_new_instance_expr.phpt', 'first_class_callable_parent_method.phpt', 'first_class_callable_parent_method_args.phpt', 'first_class_callable_parent_static.phpt', 'first_class_callable_self_static_lsb.phpt'] as $file) {
foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'arrow_fn_byref.phpt', 'arrow_fn_this_outside_context.phpt', 'nested_arrow_function.phpt', 'nested_arrow_capture_no_undefined_warning.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_recursive_use_byref.phpt', 'closure_this_binding.phpt', 'closure_auto_bind_private.phpt', 'closure_parent_method_private_scope.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_from_callable_this_private_invoke.phpt', 'closure_from_callable_class_in_closure_private.phpt', 'closure_from_callable_inaccessible.phpt', 'closure_from_callable_static_private_binds_this.phpt', 'closure_fromcallable_static_scope_typeerror.phpt', 'closure_from_callable_enum_case.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_inline_new_private.phpt', 'closure_bind_instance_inline_new.phpt', 'closure_bindto_inline_new_object.phpt', 'closure_bind_enum_case.phpt', 'closure_bindto_enum_case.phpt', 'closure_bindto_enum_scope.phpt', 'closure_bindto_null_class_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'closure_bindto_illegal_returns_null.phpt', 'closure_bindto_null_free_uses_this.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt', 'static_closure_bind.phpt', 'closure_call_static_warns.phpt', 'static_call_unbound_closure.phpt', 'closure_static_var.phpt', 'closure_static_counter.phpt', 'closure_static_use_byref.phpt', 'static_var_closure_init_fatal.phpt', 'static_var_arrow_init_fatal.phpt', 'inline_static_closure_call_arg.phpt', 'closure_call_method_rebind.phpt', 'fcc_default_parameter_compile_error.phpt', 'fcc_instance_method.phpt', 'fcc_instance_expr.phpt', 'fcc_instance_method_error.phpt', 'fcc_new_instance_expr.phpt', 'first_class_callable_parent_method.phpt', 'first_class_callable_parent_method_args.phpt', 'first_class_callable_parent_static.phpt', 'first_class_callable_self_static_lsb.phpt', 'fcc_instance_method_poisons_static.phpt'] as $file) {
$path = __DIR__ . '/cases/language/' . $file;
$name = preg_replace('/\.phpt$/', '', $file) ?: $file;
yield $name => self::parsePHPT($path, $file);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Language: instance-method FCC must not poison later static::class / : static (#32083, zend_compile.c)
--FILE--
<?php
error_reporting(E_ALL);
class FM {
public function m($x) { return $x * 2; }
}
$c = (new FM())->m(...);
echo 'fcc=', $c(4), "\n";

class Base {
public static function who() { return static::class; }
}
class Child extends Base {}
echo 'lsb=', Base::who(), ' ', Child::who(), "\n";

class A3 {
public function f(): object { return new stdClass(); }
}
class B3 extends A3 {
public function f(): static { return $this; }
}
echo 'ret=', get_class((new B3())->f()), "\n";
--EXPECT--
fcc=8
lsb=Base Child
ret=B3
26 changes: 26 additions & 0 deletions test/repro/maintainer_gap_fcc_poisons_static.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Issue #32083 — instance-method FCC must not poison later static::class / : static.
*
* php-src: Zend/zend_compile.c ZEND_AST_CALLABLE_CONVERT; Zend/zend_execute.c LSB.
*/
error_reporting(E_ALL);
class FM {
public function m($x) { return $x * 2; }
}
$c = (new FM())->m(...);
echo 'fcc=', $c(4), "\n";

class Base {
public static function who() { return static::class; }
}
class Child extends Base {}
echo 'lsb=', Base::who(), ' ', Child::who(), "\n";

class A3 {
public function f(): object { return new stdClass(); }
}
class B3 extends A3 {
public function f(): static { return $this; }
}
echo 'ret=', get_class((new B3())->f()), "\n";
61 changes: 61 additions & 0 deletions test/unit/FirstClassCallableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,67 @@ class B extends A {}
);
}

/** Issue #32083: instance-method FCC must not poison later static::class / : static. */
public function testVmInstanceMethodFccDoesNotPoisonLaterStatic(): void
{
$code = <<<'PHP'
<?php
class FM {
public function m($x) { return $x * 2; }
}
$c = (new FM())->m(...);
echo "fcc=", $c(4), "\n";
class Base {
public static function who() { return static::class; }
}
class Child extends Base {}
echo "lsb=", Base::who(), " ", Child::who(), "\n";
class A3 {
public function f(): object { return new stdClass(); }
}
class B3 extends A3 {
public function f(): static { return $this; }
}
echo "ret=", get_class((new B3())->f()), "\n";
PHP;
$rt = new Runtime();
$block = $rt->parseAndCompile($code, 'fcc_poisons_static.php');
ob_start();
$rt->run($block);
$this->assertSame(
"fcc=8\nlsb=Base Child\nret=B3\n",
ob_get_clean()
);
}

/** Issue #32083: isolated static::class / : static (no preceding FCC) still match Zend. */
public function testVmIsolatedStaticClassAndReturnTypeStillMatchZend(): void
{
$code = <<<'PHP'
<?php
class Base {
public static function who() { return static::class; }
}
class Child extends Base {}
echo "lsb=", Base::who(), " ", Child::who(), "\n";
class A3 {
public function f(): object { return new stdClass(); }
}
class B3 extends A3 {
public function f(): static { return $this; }
}
echo "ret=", get_class((new B3())->f()), "\n";
PHP;
$rt = new Runtime();
$block = $rt->parseAndCompile($code, 'isolated_static.php');
ob_start();
$rt->run($block);
$this->assertSame(
"lsb=Base Child\nret=B3\n",
ob_get_clean()
);
}

/** Issue #6851: enum case value as first-class callable must compile then Error at runtime. */
public function testVmEnumCaseValueFirstClassCallableThrowsError(): void
{
Expand Down
Loading