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
39 changes: 37 additions & 2 deletions ext/standard/JitClassAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@

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;

/** 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).
*
Expand All @@ -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, '\\'));
Expand All @@ -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).
*
Expand Down
12 changes: 12 additions & 0 deletions lib/CompilerVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down
11 changes: 9 additions & 2 deletions lib/JIT/Builtin/Type/Object_.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions lib/VM/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions test/compliance/cases/stdlib/class_alias_internal.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
if (!\PHPCompiler\CompilerVersion::allowsClassAliasOfInternalClass()) {
die('skip internal class_alias requires PHP 8.3+ profile');
}
?>
--FILE--
<?php
var_export(class_alias('stdClass', 'SC9211'));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
--TEST--
stdlib class_alias() duplicate internal name warns + false (#29084, re-#18290, Zend/zend_builtin_functions.c)
--ENV--
PHP_COMPILER_PROFILE=8.3
--SKIPIF--
<?php
if (!\PHPCompiler\CompilerVersion::allowsClassAliasOfInternalClass()) {
die('skip internal class_alias requires PHP 8.3+ profile');
}
?>
--FILE--
<?php
var_export(class_alias('stdClass', 'stdClass'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
stdlib class_alias() rejects internal originals on PROFILE≤8.2 (#29150, Zend/zend_builtin_functions.c)
--ENV--
PHP_COMPILER_PROFILE=8.2
--SKIPIF--
<?php
if (\PHPCompiler\CompilerVersion::allowsClassAliasOfInternalClass()) {
die('skip internal class_alias allowed on PHP 8.3+ profile');
}
?>
--FILE--
<?php
error_reporting(E_ALL);
try {
var_export(class_alias('stdClass', 'SC29150'));
} catch (Throwable $e) {
echo get_class($e), ':', $e->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
15 changes: 15 additions & 0 deletions test/repro/issue_29150_class_alias_internal_profile82.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
// Issue #29150 — PROFILE≤8.2 rejects class_alias of internal classes (Zend 8.1/8.2 ValueError).
error_reporting(E_ALL);
foreach (['stdClass', 'Exception', 'Traversable'] as $class) {
$alias = 'Alias29150_'.str_replace('\\', '_', $class).'_'.bin2hex(random_bytes(2));
try {
var_export(class_alias($class, $alias));
} catch (Throwable $e) {
echo get_class($e), ':', $e->getMessage();
}
echo "\n";
}
class User29150 {}
var_export(class_alias(User29150::class, 'UserAlias29150_'.bin2hex(random_bytes(2))));
echo "\n";
6 changes: 4 additions & 2 deletions test/unit/ClassAliasInternal29084Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand Down
65 changes: 65 additions & 0 deletions test/unit/ClassAliasInternalProfile29150Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

/**
* class_alias() of internal classes is profile-gated (#29150, re-#29084).
*
* php-src: Zend/zend_builtin_functions.c — PHP_FUNCTION(class_alias)
*/
final class ClassAliasInternalProfile29150Test extends TestCase
{
private const VALUE_ERROR =
'ValueError:class_alias(): Argument #1 ($class) must be a user-defined class name, internal class name given';

public function testVmRejectsInternalOnProfile82(): void
{
$lines = $this->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<string>
*/
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;
}
}
47 changes: 47 additions & 0 deletions test/unit/CompilerVersionGateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading