diff --git a/lib/JIT.php b/lib/JIT.php index 4425ad3b663..fb557de6ce2 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -6141,6 +6141,15 @@ private function compileBlockInternal( $callProxy, $internalName ); + $isStaticClosure = null !== $op->block1->func + && (($op->block1->func->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0; + if ($isStaticClosure) { + $closureObj->closureIsStatic = true; + JIT\ClosureBindHelper::storeStaticClosureFlag( + $this->context, + $this->context->helper->loadValue($closureObj) + ); + } if (null !== $block->func && null !== $block->func->class) { JIT\ClosureBindHelper::ensureClosureBindingProperties($this->context); @@ -6154,8 +6163,6 @@ private function compileBlockInternal( ); $boundScope->compileTimeString = $scopeName; - $isStaticClosure = null !== $op->block1->func - && (($op->block1->func->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0; $boundThis = JIT\ClosureHelper::nullCapture($this->context); if (!$isStaticClosure) { $thisVar = $this->context->variableForScopedName('this'); diff --git a/lib/JIT/ClosureBindHelper.php b/lib/JIT/ClosureBindHelper.php index 7c9af2af01d..1e493974f22 100644 --- a/lib/JIT/ClosureBindHelper.php +++ b/lib/JIT/ClosureBindHelper.php @@ -4,6 +4,7 @@ namespace PHPCompiler\JIT; +use PHPCompiler\JIT\Builtin\ErrorRaise; use PHPCompiler\JIT\Builtin\TypeErrorRaise; use PHPCompiler\JIT\Call; use PHPCompiler\JIT\Call\ClosureWithBinding; @@ -21,6 +22,8 @@ final class ClosureBindHelper public const BOUND_SCOPE_PROPERTY = '__closure_bound_scope'; + public const IS_STATIC_PROPERTY = '__closure_is_static'; + public static function registerJitMethods(Context $context): void { $context->functionProxies['closure::bindto'] = new Call\ClosureBindTo(); @@ -37,6 +40,9 @@ public static function ensureClosureBindingProperties(Context $context): void if (!$objectType->hasProperty($classId, self::BOUND_SCOPE_PROPERTY)) { $objectType->defineProperty($classId, self::BOUND_SCOPE_PROPERTY, Variable::TYPE_STRING); } + if (!$objectType->hasProperty($classId, self::IS_STATIC_PROPERTY)) { + $objectType->defineProperty($classId, self::IS_STATIC_PROPERTY, Variable::TYPE_NATIVE_BOOL); + } } public static function bind( @@ -56,6 +62,7 @@ public static function bind( } $inner = self::resolveInnerCall($context, $closure); + self::assertNotBindingStaticClosureToObject($context, $closure, $inner, $newThis); if (null === $inner) { return self::nullResult($context); } @@ -69,6 +76,7 @@ public static function bind( $boundThis, $boundScope ); + $result->closureIsStatic = $closure->closureIsStatic; return $result; } @@ -231,10 +239,36 @@ private static function cloneClosureObject( self::BOUND_SCOPE_PROPERTY, $boundScope ); + $staticFlag = $context->type->object->propertyFetch( + $srcObj, + 'Closure', + self::IS_STATIC_PROPERTY + ); + $context->type->object->storeInstanceProperty( + $dest, + 'Closure', + self::IS_STATIC_PROPERTY, + $staticFlag + ); return $dest; } + public static function storeStaticClosureFlag(Context $context, Value $closureObj): void + { + self::ensureClosureBindingProperties($context); + $i1 = $context->getTypeFromString('int1'); + $trueLit = $context->builder->load($i1->constInt(1, false)); + $trueVar = new Variable($context, Variable::TYPE_NATIVE_BOOL, Variable::KIND_VALUE, $trueLit); + $trueVar->addref(); + $context->type->object->storeInstanceProperty( + $closureObj, + 'Closure', + self::IS_STATIC_PROPERTY, + $trueVar + ); + } + private static function loadClosureObject(Context $context, Variable $closure): Value { if (Variable::TYPE_OBJECT === $closure->type) { @@ -580,6 +614,104 @@ private static function scalarLabel(Variable $arg): string }; } + private static function assertNotBindingStaticClosureToObject( + Context $context, + Variable $closure, + ?Call $inner, + Variable $newThis + ): void { + if ($closure->closureIsStatic) { + self::assertNotBindingObjectToStaticClosure($context, $newThis); + + return; + } + if (Variable::TYPE_OBJECT === $closure->type || Variable::TYPE_VALUE === $closure->type) { + self::emitClosureObjectStaticBindGuard($context, $closure, $newThis); + } + } + + private static function assertNotBindingObjectToStaticClosure(Context $context, Variable $newThis): void + { + if (Variable::TYPE_NULL === $newThis->type || ($newThis->isNullConstant ?? false)) { + return; + } + if (Variable::TYPE_OBJECT === $newThis->type) { + self::raiseStaticBindError($context); + + return; + } + if (Variable::TYPE_VALUE === $newThis->type) { + self::emitValueBoxStaticBindGuard($context, $newThis); + } + } + + private static function emitClosureObjectStaticBindGuard( + Context $context, + Variable $closure, + Variable $newThis + ): void { + self::ensureClosureBindingProperties($context); + $obj = self::loadClosureObject($context, $closure); + $flag = $context->type->object->propertyFetch( + $obj, + 'Closure', + self::IS_STATIC_PROPERTY + ); + if (Variable::TYPE_NATIVE_BOOL !== $flag->type || Variable::KIND_VALUE !== $flag->kind) { + return; + } + $i1 = $context->getTypeFromString('int1'); + $isStatic = $context->builder->icmp( + Builder::INT_NE, + $context->helper->loadValue($flag), + $i1->constInt(0, false) + ); + $skipBlock = BasicBlockHelper::append($context, 'closure_bind_not_static'); + $checkBlock = BasicBlockHelper::append($context, 'closure_bind_static_check'); + $context->builder->branchIf($isStatic, $checkBlock, $skipBlock); + $context->builder->positionAtEnd($checkBlock); + self::assertNotBindingObjectToStaticClosure($context, $newThis); + $context->builder->positionAtEnd($skipBlock); + } + + private static function raiseStaticBindError(Context $context): void + { + ErrorRaise::registerDeclarations($context); + ErrorRaise::ensureLinked($context); + ErrorRaise::emitRaise($context, 'Cannot bind static closure to object'); + $context->builder->call($context->lookupFunction('abort')); + } + + private static function emitValueBoxStaticBindGuard(Context $context, Variable $newThis): void + { + $ptr = JitValueBox::valuePtrFromVariable($context, $newThis); + $typeByte = self::loadValueTypeByte($context, $ptr); + $i8 = $context->getTypeFromString('int8'); + $nullBlock = BasicBlockHelper::append($context, 'closure_bind_static_null'); + $objBlock = BasicBlockHelper::append($context, 'closure_bind_static_obj'); + $mergeBlock = BasicBlockHelper::append($context, 'closure_bind_static_merge'); + $isNull = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NULL, false) + ); + $context->builder->branchIf($isNull, $nullBlock, $objBlock); + $context->builder->positionAtEnd($nullBlock); + $context->builder->branch($mergeBlock); + $context->builder->positionAtEnd($objBlock); + $isObj = $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_OBJECT, false) + ); + $okBlock = BasicBlockHelper::append($context, 'closure_bind_static_ok'); + $context->builder->branchIf($isObj, $okBlock, $mergeBlock); + $context->builder->positionAtEnd($okBlock); + self::raiseStaticBindError($context); + $context->builder->branch($mergeBlock); + $context->builder->positionAtEnd($mergeBlock); + } + private static function thisArgLabel(string $context): string { return 'Closure::bind()' === $context ? '#2 ($newThis)' : '#1 ($newThis)'; diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index a746fac99f9..62bbae1d141 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -145,6 +145,9 @@ final class Variable { /** Native call proxy when this object is a JIT-lowered closure (#72). */ public ?Call $closureCall = null; + /** Anonymous `static function` — cannot bind $this (Zend zend_closures.c, #4613). */ + public bool $closureIsStatic = false; + /** Heap {@see __generator_state__*} for JIT Generator objects (#3074). */ public ?\PHPLLVM\Value $generatorStatePtr = null; diff --git a/lib/VM/ClosureState.php b/lib/VM/ClosureState.php index 1010a1f5b66..3a9cb61ae9b 100644 --- a/lib/VM/ClosureState.php +++ b/lib/VM/ClosureState.php @@ -93,6 +93,21 @@ public function isUserClosure(): bool return null === $this->wrappedFunc && null === $this->methodName; } + /** Zend zend_closure_bind(): static closures cannot receive a bound $this. */ + public function isStaticClosure(): bool + { + $compilerFunc = $this->wrappedFunc ?? $this->func; + if (!$compilerFunc instanceof Func\PHP) { + return false; + } + $cfgFunc = $compilerFunc->block->func ?? null; + if (null === $cfgFunc) { + return false; + } + + return (($cfgFunc->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0; + } + public static function register(Context $ctx): void { $entry = new ClassEntry('Closure'); diff --git a/lib/VM/ClosureSupport.php b/lib/VM/ClosureSupport.php index 7f99a6267cc..eaf4ced0a77 100644 --- a/lib/VM/ClosureSupport.php +++ b/lib/VM/ClosureSupport.php @@ -110,6 +110,9 @@ public static function bindTo( if (null !== $state->wrappedFunc || null !== $state->methodName) { return null; } + if (Variable::TYPE_OBJECT === $newThis->type && $state->isStaticClosure()) { + throw new \Error('Cannot bind static closure to object'); + } $bound = $state->cloneForBind(); if (Variable::TYPE_NULL === $newThis->type) { $bound->boundThis = null; diff --git a/test/compliance/ClosureVMTest.php b/test/compliance/ClosureVMTest.php index 28092e53282..66ab87dc508 100644 --- a/test/compliance/ClosureVMTest.php +++ b/test/compliance/ClosureVMTest.php @@ -16,7 +16,7 @@ public function setUp(): void public static function providePHPTests(): \Generator { - foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_this_binding.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt'] as $file) { + foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_this_binding.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt', 'static_closure_bind.phpt'] as $file) { $path = __DIR__ . '/cases/language/' . $file; $name = preg_replace('/\.phpt$/', '', $file) ?: $file; yield $name => self::parsePHPT($path, $file); diff --git a/test/compliance/cases/language/static_closure_bind.phpt b/test/compliance/cases/language/static_closure_bind.phpt new file mode 100644 index 00000000000..f3b9e9a50e9 --- /dev/null +++ b/test/compliance/cases/language/static_closure_bind.phpt @@ -0,0 +1,37 @@ +--TEST-- +language: static closure bindTo/bind must Error (issue #4613, Zend/zend_closures.c) +--FILE-- +make(); +try { + $fn->bindTo($c); + echo "bindTo ok\n"; +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + Closure::bind($fn, $c, 'C'); + echo "bind ok\n"; +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +$unbound = $fn->bindTo(null); +echo $unbound === null ? "null\n" : "object\n"; +--EXPECT-- +Error: Cannot bind static closure to object +Error: Cannot bind static closure to object +object