From b9740ddc1b91170aaa6085acb54d5a9b92e8ea59 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 8 Aug 2026 22:27:11 +0000 Subject: [PATCH] =?UTF-8?q?Language:=20class=5Falias=20internal=20original?= =?UTF-8?q?s=20ValueError=20on=20PROFILE=E2=89=A48.2=20(#29150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate Zend 8.1/8.2 rejection behind CompilerVersion::allowsClassAliasOfInternalClass while keeping the #29084 allow path for PROFILE≥8.3. Co-authored-by: Cursor --- ext/standard/JitClassAlias.php | 39 ++++++++++- lib/CompilerVersion.php | 12 ++++ lib/JIT/Builtin/Type/Object_.php | 11 +++- lib/VM/Context.php | 10 ++- .../cases/stdlib/class_alias_internal.phpt | 8 +++ .../class_alias_internal_duplicate.phpt | 8 +++ ...class_alias_internal_reject_profile82.phpt | 32 +++++++++ ...e_29150_class_alias_internal_profile82.php | 15 +++++ test/unit/ClassAliasInternal29084Test.php | 6 +- .../ClassAliasInternalProfile29150Test.php | 65 +++++++++++++++++++ test/unit/CompilerVersionGateTest.php | 47 ++++++++++++++ 11 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 test/compliance/cases/stdlib/class_alias_internal_reject_profile82.phpt create mode 100644 test/repro/issue_29150_class_alias_internal_profile82.php create mode 100644 test/unit/ClassAliasInternalProfile29150Test.php diff --git a/ext/standard/JitClassAlias.php b/ext/standard/JitClassAlias.php index c166378f649..60acf7db66a 100644 --- a/ext/standard/JitClassAlias.php +++ b/ext/standard/JitClassAlias.php @@ -4,7 +4,9 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\CompilerVersion; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\ExceptionBridge; use PHPCompiler\JIT\JitStringArg; use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; @@ -12,6 +14,10 @@ /** LLVM lowering for class_alias() (issues #3095, #3178, #6583). */ final class JitClassAlias { + /** php-src Zend/zend_builtin_functions.c — zif_class_alias internal-class ValueError (#29150). */ + public const INTERNAL_CLASS_VALUE_ERROR = + 'class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given'; + /** * Compile-time string operands (issues #3095, #3178). * @@ -31,6 +37,15 @@ public static function invokeLiteral( $autoload = 0 !== (int) $context->llvm->lib->LLVMConstIntGetZExtValue($autoloadArg->value->value); } + $i1 = $context->getTypeFromString('int1'); + // PROFILE≤8.2: emit catchable ValueError in IR (do not throw in the host compiler) (#29150). + if (!CompilerVersion::allowsClassAliasOfInternalClass() + && self::originalIsInternalClass($context, $original)) { + ExceptionBridge::emitValueErrorAndAbort($context, self::INTERNAL_CLASS_VALUE_ERROR); + + return $i1->constInt(0, false); + } + $object = $context->type->object; $originalLc = strtolower(ltrim($original, '\\')); $aliasLc = strtolower(ltrim($alias, '\\')); @@ -53,11 +68,31 @@ public static function invokeLiteral( } } - $i1 = $context->getTypeFromString('int1'); - return $i1->constInt($ok ? 1 : 0, false); } + /** Whether the class_alias() original resolves to an internal class/interface (#29150). */ + private static function originalIsInternalClass(Context $context, string $original): bool + { + $originalLc = strtolower(ltrim($original, '\\')); + $object = $context->type->object; + $classId = $object->classIdForLowerName($originalLc); + if (null !== $classId && $object->isExternalOnlyClass($classId)) { + return true; + } + + $vmContext = $context->runtime->vmContext; + if (null === $vmContext || !isset($vmContext->classes[$originalLc])) { + return false; + } + $canonicalOriginalLc = $originalLc; + while (isset($vmContext->classAliases[$canonicalOriginalLc])) { + $canonicalOriginalLc = $vmContext->classAliases[$canonicalOriginalLc]; + } + + return $vmContext->classes[$canonicalOriginalLc]->isInternal; + } + /** * Runtime string operands — php-src Z_PARAM_STR via {@see JitStringBuiltinArg} (#6583). * diff --git a/lib/CompilerVersion.php b/lib/CompilerVersion.php index 0e02836ac88..622a0035468 100644 --- a/lib/CompilerVersion.php +++ b/lib/CompilerVersion.php @@ -2206,6 +2206,18 @@ public static function advertisesSocketAtmark(): bool return self::supportsSocketAtmark(); } + /** + * Zend 8.3+ allows {@code class_alias()} of internal classes; 8.1/8.2 throw ValueError (#29150). + * + * Uses {@see languageProfileVersion()} so explicit {@code PHP_COMPILER_PROFILE=8.1}/{@code 8.2} + * reject while unset PROFILE on 8.4.0-dev and PROFILE≥8.3 keep the #29084 allow path. + * php-src: Zend/zend_builtin_functions.c — PHP_FUNCTION(class_alias). + */ + public static function allowsClassAliasOfInternalClass(): bool + { + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); + } + /** * PHP 8.5+ sockets SHUT_RD / SHUT_WR / SHUT_RDWR (ext/sockets/sockets.stub.php; #26760). * diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 2e00f8a3411..9bb48f91861 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -2105,8 +2105,9 @@ public function hasUserDeclaredClass(string $name): bool /** * Register an alternate name for a JIT-known class (class_alias, #3178). * - * php-src: zend_register_class_alias_ex — allows internal originals; alias-of-alias to - * canonical (#11639). Duplicate names warn + false (#29084 / re-#18290). + * php-src: zend_register_class_alias_ex — internal originals allowed on PHP 8.3+ (#29084); + * PROFILE≤8.2 throws ValueError (#29150). Alias-of-alias to canonical (#11639). + * Duplicate names warn + false (#29084 / re-#18290). */ public function registerClassAlias(string $original, string $alias): bool { @@ -2130,6 +2131,12 @@ public function registerClassAlias(string $original, string $alias): bool } $classId = $this->classes[$canonicalOriginalLc]; + if (isset($this->externalOnlyClassIds[$classId]) + && !\PHPCompiler\CompilerVersion::allowsClassAliasOfInternalClass()) { + throw new \ValueError( + 'class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given' + ); + } if (isset($this->classes[$aliasLc]) || isset($this->classAliasToOriginalLc[$aliasLc])) { $vmContext = $this->context->runtime->vmContext ?? null; diff --git a/lib/VM/Context.php b/lib/VM/Context.php index d16774a43e9..9f9da6c0d40 100755 --- a/lib/VM/Context.php +++ b/lib/VM/Context.php @@ -664,8 +664,9 @@ private static function isResolvableRegisteredFunction(string $lcname): bool /** * Register an alternate name for a class (ext/standard class_alias, #3095). * - * php-src: zend_register_class_alias_ex — allows internal originals (stdClass, Exception); - * resolves alias-of-alias to canonical class (#11639). Duplicate alias names warn + false (#29084). + * php-src: zend_register_class_alias_ex — internal originals allowed on PHP 8.3+ (#29084); + * PROFILE≤8.2 throws ValueError (#29150). Alias-of-alias resolves to canonical (#11639). + * Duplicate alias names warn + false (#29084 / re-#18290). */ public function registerClassAlias(string $original, string $alias, bool $autoload = true, ?\PHPCompiler\Frame $frame = null): bool { @@ -703,6 +704,11 @@ public function registerClassAlias(string $original, string $alias, bool $autolo } $entry = $this->classes[$canonicalOriginalLc]; + if ($entry->isInternal && !\PHPCompiler\CompilerVersion::allowsClassAliasOfInternalClass()) { + throw new \ValueError( + 'class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given' + ); + } if (isset($this->classes[$aliasLc]) || isset($this->classAliases[$aliasLc]) || isset($this->enums[$aliasLc])) { $this->errors->triggerError( diff --git a/test/compliance/cases/stdlib/class_alias_internal.phpt b/test/compliance/cases/stdlib/class_alias_internal.phpt index b3b7c2d5f69..882a520f924 100644 --- a/test/compliance/cases/stdlib/class_alias_internal.phpt +++ b/test/compliance/cases/stdlib/class_alias_internal.phpt @@ -1,5 +1,13 @@ --TEST-- stdlib class_alias() allows internal class originals (#29084, re-#9211/#18290, Zend/zend_builtin_functions.c) +--ENV-- +PHP_COMPILER_PROFILE=8.3 +--SKIPIF-- + --FILE-- --FILE-- +--FILE-- +getMessage(); +} +echo "\n"; +try { + var_export(class_alias('Exception', 'E29150')); +} catch (Throwable $e) { + echo get_class($e), ':', $e->getMessage(); +} +echo "\n"; +class User29150 {} +var_export(class_alias(User29150::class, 'U29150')); +echo "\n"; +--EXPECT-- +ValueError:class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given +ValueError:class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given +true diff --git a/test/repro/issue_29150_class_alias_internal_profile82.php b/test/repro/issue_29150_class_alias_internal_profile82.php new file mode 100644 index 00000000000..145b814c459 --- /dev/null +++ b/test/repro/issue_29150_class_alias_internal_profile82.php @@ -0,0 +1,15 @@ +getMessage(); + } + echo "\n"; +} +class User29150 {} +var_export(class_alias(User29150::class, 'UserAlias29150_'.bin2hex(random_bytes(2)))); +echo "\n"; diff --git a/test/unit/ClassAliasInternal29084Test.php b/test/unit/ClassAliasInternal29084Test.php index bffd5e860b2..d7e6967ffb5 100644 --- a/test/unit/ClassAliasInternal29084Test.php +++ b/test/unit/ClassAliasInternal29084Test.php @@ -20,7 +20,8 @@ public function testVmAllowsInternalClassAliasAndDuplicateWarns(): void { $root = dirname(__DIR__, 2); $src = $root.'/test/repro/issue_29084_class_alias_internal.php'; - $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/bin/vm.php') + $cmd = 'PHP_COMPILER_PROFILE=8.4 ' + .escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/bin/vm.php') .' '.escapeshellarg($src).' 2>&1'; exec($cmd, $out, $rc); $this->assertSame(0, $rc, implode("\n", $out)); @@ -34,7 +35,8 @@ public function testJitAllowsInternalClassAliasAndDuplicateWarns(): void { $root = dirname(__DIR__, 2); $src = $root.'/test/repro/issue_29084_class_alias_internal.php'; - $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/bin/jit.php') + $cmd = 'PHP_COMPILER_PROFILE=8.4 ' + .escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/bin/jit.php') .' '.escapeshellarg($src).' 2>&1'; exec($cmd, $out, $rc); $this->assertSame(0, $rc, implode("\n", $out)); diff --git a/test/unit/ClassAliasInternalProfile29150Test.php b/test/unit/ClassAliasInternalProfile29150Test.php new file mode 100644 index 00000000000..2ab54c2224d --- /dev/null +++ b/test/unit/ClassAliasInternalProfile29150Test.php @@ -0,0 +1,65 @@ +runReproLines('bin/vm.php', '8.2'); + $this->assertSame( + [self::VALUE_ERROR, self::VALUE_ERROR, self::VALUE_ERROR, 'true'], + $lines + ); + } + + public function testJitRejectsInternalOnProfile82(): void + { + $lines = $this->runReproLines('bin/jit.php', '8.2'); + $this->assertSame( + [self::VALUE_ERROR, self::VALUE_ERROR, self::VALUE_ERROR, 'true'], + $lines + ); + } + + public function testVmAllowsInternalOnProfile84(): void + { + $lines = $this->runReproLines('bin/vm.php', '8.4'); + $this->assertSame(['true', 'true', 'true', 'true'], $lines); + } + + /** + * @return list + */ + private function runReproLines(string $bin, string $profile): array + { + $root = dirname(__DIR__, 2); + $src = $root.'/test/repro/issue_29150_class_alias_internal_profile82.php'; + $cmd = 'PHP_COMPILER_PROFILE='.escapeshellarg($profile).' ' + .escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/'.$bin) + .' '.escapeshellarg($src).' 2>&1'; + exec($cmd, $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + $lines = []; + foreach ($out as $line) { + if (str_starts_with($line, 'PHP Deprecated:')) { + continue; + } + $lines[] = $line; + } + + return $lines; + } +} diff --git a/test/unit/CompilerVersionGateTest.php b/test/unit/CompilerVersionGateTest.php index d4748d3fb1a..3bc8dd16202 100644 --- a/test/unit/CompilerVersionGateTest.php +++ b/test/unit/CompilerVersionGateTest.php @@ -1247,6 +1247,53 @@ public function testSupportsSocketAtmarkTrueOnForwardProfile(): void } } + public function testAllowsClassAliasOfInternalClassDefaultAnd84(): void + { + // Unset PROFILE on 8.4.0-dev keeps #29084 allow (languageProfileVersion ≥ 8.3.0-dev). + $this->assertTrue(CompilerVersion::allowsClassAliasOfInternalClass()); + $prev = getenv('PHP_COMPILER_PROFILE'); + putenv('PHP_COMPILER_PROFILE=8.4'); + try { + $this->assertTrue(CompilerVersion::allowsClassAliasOfInternalClass()); + } finally { + if (false === $prev) { + putenv('PHP_COMPILER_PROFILE'); + } else { + putenv('PHP_COMPILER_PROFILE='.$prev); + } + } + } + + public function testAllowsClassAliasOfInternalClassFalseOn82(): void + { + $prev = getenv('PHP_COMPILER_PROFILE'); + putenv('PHP_COMPILER_PROFILE=8.2'); + try { + $this->assertFalse(CompilerVersion::allowsClassAliasOfInternalClass()); + } finally { + if (false === $prev) { + putenv('PHP_COMPILER_PROFILE'); + } else { + putenv('PHP_COMPILER_PROFILE='.$prev); + } + } + } + + public function testAllowsClassAliasOfInternalClassTrueOn83(): void + { + $prev = getenv('PHP_COMPILER_PROFILE'); + putenv('PHP_COMPILER_PROFILE=8.3'); + try { + $this->assertTrue(CompilerVersion::allowsClassAliasOfInternalClass()); + } finally { + if (false === $prev) { + putenv('PHP_COMPILER_PROFILE'); + } else { + putenv('PHP_COMPILER_PROFILE='.$prev); + } + } + } + public function testSupportsSocketShutConstantsFalseOnDefaultAnd84(): void { $this->assertFalse(CompilerVersion::supportsSocketShutConstants());