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
94 changes: 55 additions & 39 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -16741,28 +16741,11 @@ protected function initStaticCallable(
$this->assertMethodCallableStatically($class, $methodLc);
}
} catch (\LogicException $e) {
$magicClass = $this->findMagicCallStaticClass($lcClass);
if (null === $magicClass) {
throw $e;
}
$frame->magicCallMethodName = $methodName;
$vis = $magicClass->methodVisibility['__callstatic'] ?? \PHPCfg\Func::FLAG_PUBLIC;
$callerClassLc = null;
if (null !== $frame->block->func && null !== $frame->block->func->class) {
$callerClassLc = strtolower($frame->block->func->class->value);
// Missing method → zend_std_get_static_method slow path → __callStatic (#3273).
if ($this->tryDispatchCallStatic($frame, $lcClass, $methodName)) {
return;
}
MethodVisibility::assertCallable(
$vis,
$callerClassLc,
strtolower($magicClass->name),
$magicClass->name,
'__callStatic'
);
$frame->call = $magicClass->methods['__callstatic'];
$frame->callArgs = [];
$frame->callArgEntries = [];

return;
throw $e;
}
$vis = $class->methodVisibility[$methodLc] ?? \PHPCfg\Func::FLAG_PUBLIC;
$callerClassLc = $this->callerClassLc($frame);
Expand All @@ -16778,23 +16761,62 @@ protected function initStaticCallable(
}
$declaredName = $class->methodNames[$methodLc] ?? $methodName;
$callerDisplay = $this->callerScopeDisplay($frame, $callerClassLc);
// zend_vm_def.h: INIT_STATIC_METHOD_CALL + CONSTRUCTOR → "Cannot call private …::__construct()" (#25663).
$staticConstructorCall = '__construct' === $methodLc;
try {
MethodVisibility::assertCallable(
$vis,
$callerClassLc,
strtolower($class->name),
$class->name,
$declaredName,
$parentScopeAllows,
fn (string $classLc, string $ancestorLc): bool => $this->isClassSameOrSubclassOf($classLc, $ancestorLc),
$callerDisplay
);
} catch (\LogicException $e) {
// Inaccessible private/protected static → same __callStatic fallback as missing
// methods (php-src get_static_method_fallback / #25670, re-#3273).
if ($this->tryDispatchCallStatic($frame, $lcClass, $methodName)) {
return;
}
throw $e;
}
$frame->call = $class->methods[$methodLc];
$frame->callArgs = $this->callArgsForStaticMethod($frame, $lcClass, $frame->call, $parentKeywordScope);
$frame->callArgEntries = [];
$frame->builtinCalleeQualifiedMethod = $class->name.'::'.$declaredName;
}

/**
* Bind a static call to __callStatic when present (Zend get_static_method_fallback).
*
* Used for both missing methods (#3273) and inaccessible private/protected statics (#25670).
*
* @return bool true when the frame was bound to __callStatic
*/
private function tryDispatchCallStatic(Frame $frame, string $lcClass, string $methodName): bool
{
$magicClass = $this->findMagicCallStaticClass($lcClass);
if (null === $magicClass) {
return false;
}
$frame->magicCallMethodName = $methodName;
$vis = $magicClass->methodVisibility['__callstatic'] ?? \PHPCfg\Func::FLAG_PUBLIC;
$callerClassLc = null;
if (null !== $frame->block->func && null !== $frame->block->func->class) {
$callerClassLc = strtolower($frame->block->func->class->value);
}
MethodVisibility::assertCallable(
$vis,
$callerClassLc,
strtolower($class->name),
$class->name,
$declaredName,
$parentScopeAllows,
fn (string $classLc, string $ancestorLc): bool => $this->isClassSameOrSubclassOf($classLc, $ancestorLc),
$callerDisplay,
$staticConstructorCall
strtolower($magicClass->name),
$magicClass->name,
'__callStatic'
);
$frame->call = $class->methods[$methodLc];
$frame->callArgs = $this->callArgsForStaticMethod($frame, $lcClass, $frame->call, $parentKeywordScope);
$frame->call = $magicClass->methods['__callstatic'];
$frame->callArgs = [];
$frame->callArgEntries = [];
$frame->builtinCalleeQualifiedMethod = $class->name.'::'.$declaredName;

return true;
}

/**
Expand Down Expand Up @@ -18098,12 +18120,6 @@ protected function inheritFromParent(ClassEntry $entry): void
$this->rejectIncompatibleChildMethodSignature($entry, $parent, $name);
continue;
}
// Child redeclared a concrete parent method as abstract (zend_inheritance.c, #25660).
// Abstract decls live in abstractMethods, not methods — still enforce before inherit.
if (isset($entry->abstractMethods[$name])) {
$this->rejectIncompatibleChildMethodSignature($entry, $parent, $name);
continue;
}
// PDO_*_Ext driver methods stay on PDO only (#21552).
if (isset($parent->methodNotInherited[$name])) {
continue;
Expand Down
37 changes: 37 additions & 0 deletions test/compliance/cases/language/magic_callstatic_private.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
language: inaccessible private/protected static → __callStatic (issue #25670, re-#3273)
--FILE--
<?php
class A {
private static function hid() { return "p"; }
protected static function prot() { return "pr"; }
public static function __callStatic($n, $a) {
echo "CS_$n\n";
return "m";
}
public static function inside() {
return self::hid();
}
}
class B extends A {
public static function fromChild() {
return parent::prot();
}
public static function childPriv() {
return parent::hid();
}
}
echo A::hid(), "\n";
echo A::prot(), "\n";
echo A::inside(), "\n";
echo B::fromChild(), "\n";
echo B::childPriv(), "\n";
--EXPECT--
CS_hid
m
CS_prot
m
p
pr
CS_hid
m
28 changes: 28 additions & 0 deletions test/repro/issue_25670_callstatic_private.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Repro for #25670 — outside-scope private/protected static must dispatch __callStatic.
*/
class A {
private static function hid() { return "p"; }
protected static function prot() { return "pr"; }
public static function __callStatic($n, $a) {
echo "CS_$n\n";
return "m";
}
public static function inside() {
return self::hid();
}
}
class B extends A {
public static function fromChild() {
return parent::prot();
}
public static function childPriv() {
return parent::hid();
}
}
echo A::hid(), "\n";
echo A::prot(), "\n";
echo A::inside(), "\n";
echo B::fromChild(), "\n";
echo B::childPriv(), "\n";
Loading