From ba8c96712a02607d85739d36cf9c5addc914bf22 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 10 Jun 2026 15:43:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(optimizer):=20=E8=A7=A3=E5=86=B3=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=B1=9E=E6=80=A7=E4=BC=98=E5=8C=96=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=B0=83=E7=94=A8=E5=AE=89=E5=85=A8=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了对 eval 和 include 语句的对象属性访问保护 - 实现了内部函数和静态方法调用的安全性检查机制 - 新增了 isSafeObjectExposureCall 方法来判断安全的对象暴露调用 - 修复了对象别名和动态属性操作中的潜在风险 - 添加了相关的单元测试验证安全性检查逻辑 --- phpunit/src/SsaAnalysisTest.php | 56 ++++++++++++++ src/Php/Optimizer/SsaPropOptimizer.php | 75 +++++++++++++++++-- .../objprop-hoist-this-object-arg-escape.phpt | 8 +- .../static-prop-dynamic-call-stable.phpt | 27 +++++++ 4 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 tests/aot/static/static-prop-dynamic-call-stable.phpt diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index ab7b6f0e..ef7fe47e 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -819,6 +819,62 @@ class SsaAnalysisTest extends TestCase $this->assertSame(['a' => true], $result); } + public function testCollectDangerousPropOpsInternalFunctionObjectArgumentIsSafe(): void + { + $funcCall = new Expr\FuncCall(new Node\Name('gettype'), [new Arg(new Expr\Variable('obj'))]); + $stmt = new Stmt\Expression($funcCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame([], $result); + } + + public function testCollectDangerousPropOpsInternalStaticMethodObjectArgumentIsSafe(): void + { + $staticCall = new Expr\StaticCall( + new Node\Name('DateTimeImmutable'), + 'createFromMutable', + [new Arg(new Expr\Variable('obj'))] + ); + $stmt = new Stmt\Expression($staticCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame([], $result); + } + + public function testCollectDangerousPropOpsEvalInvalidatesLaterPropertyAccess(): void + { + $eval = new Expr\Eval_(new Scalar\String_('$obj->a = 99;')); + $stmt = new Stmt\Expression($eval); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame(['a' => true], $result); + } + + public function testCollectDangerousPropOpsIncludeInvalidatesLaterPropertyAccess(): void + { + $include = new Expr\Include_(new Scalar\String_('unknown.php'), Expr\Include_::TYPE_INCLUDE); + $stmt = new Stmt\Expression($include); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame(['a' => true], $result); + } + public function testCollectDangerousPropOpsObjectAliasWildcard(): void { $assign = new Expr\Assign(new Expr\Variable('alias'), new Expr\Variable('obj')); diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index 8935f1ba..b6b56b28 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -23,6 +23,7 @@ namespace PhpAot\Php\Optimizer; use PhpAot\Php\Analysis\SsaBuilder; use PhpAot\Php\Analysis\SsaFlags; +use PhpAot\Php\Reflection; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; @@ -349,6 +350,12 @@ trait SsaPropOptimizer } } + if ($node instanceof Expr\Eval_ || $node instanceof Expr\Include_) { + $this->collectPropEvents($node->expr, $objName, $events); + $events[] = ['kind' => 'danger', 'prop' => '*']; + return; + } + if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall || $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) { if ($node instanceof Expr\StaticCall && $node->class instanceof Expr) { @@ -366,14 +373,16 @@ trait SsaPropOptimizer $this->collectPropEvents($arg->value, $objName, $events); } } - if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) - && $this->isVarNamed($node->var, $objName)) { - $events[] = ['kind' => 'danger', 'prop' => '*']; - } - foreach ($node->args as $arg) { - if ($this->exprMayExposeObject($arg->value, $objName)) { + if (!$this->isSafeObjectExposureCall($node)) { + if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) + && $this->isVarNamed($node->var, $objName)) { $events[] = ['kind' => 'danger', 'prop' => '*']; } + foreach ($node->args as $arg) { + if ($this->exprMayExposeObject($arg->value, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; + } + } } return; } @@ -430,6 +439,60 @@ trait SsaPropOptimizer } } + protected function isSafeObjectExposureCall(Expr\FuncCall|Expr\MethodCall|Expr\StaticCall|Expr\NullsafeMethodCall $node): bool + { + if ($node instanceof Expr\FuncCall) { + return $node->name instanceof Node\Name + && $this->isInternalFunctionName($node->name); + } + + if ($node instanceof Expr\StaticCall) { + return $node->class instanceof Node\Name + && $node->name instanceof Node\Identifier + && $this->isInternalClassCall($this->resolveStaticCallClassForSafety($node->class), $node->name->toString()); + } + + if (!$node->name instanceof Node\Identifier) { + return false; + } + + $className = $this->detectClassOfExpr($node->var); + return $this->isInternalClassCall($className, $node->name->toString()); + } + + protected function isInternalFunctionName(Node\Name $name): bool + { + $functionName = ltrim($name->toString(), '\\'); + + if (str_contains($functionName, '\\')) { + return false; + } + + return $this->isInternalFunction($functionName) || $this->isInternalFunction(strtolower($functionName)); + } + + protected function resolveStaticCallClassForSafety(Node\Name $classNode): string + { + $className = $classNode->toString(); + if ($className === 'self') { + return $this->classDef ? $this->getFullClassName() : ''; + } + if ($className === 'parent') { + return $this->classDef ? $this->classDef->extends : ''; + } + if ($className === 'static') { + return ''; + } + return $this->getNamespacedClassName($className); + } + + protected function isInternalClassCall(string $className, string $methodName): bool + { + return $className !== '' + && ($this->isInternalClass($className) || $this->isInternalInterface($className)) + && Reflection::hasMethod($className, $methodName); + } + /** * Dynamic property name expressions can contain normal property reads: * unset($o->{$other->name}) should still record $other->name if relevant. diff --git a/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt b/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt index d5f89318..a28036fd 100644 --- a/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt +++ b/tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt @@ -10,19 +10,13 @@ class Foo { public function run(): void { $this->a = 1; - $fn = 'make_ref'; - $fn($this); + eval('function make_ref($o): void { $ref =& $o->a; $ref = 99; } make_ref($this);'); $this->a += 1; var_dump($this->a); } } -function make_ref(Foo $o): void { - $ref =& $o->a; - $ref = 99; -} - function main(): void { (new Foo())->run(); } diff --git a/tests/aot/static/static-prop-dynamic-call-stable.phpt b/tests/aot/static/static-prop-dynamic-call-stable.phpt new file mode 100644 index 00000000..4af7f3d6 --- /dev/null +++ b/tests/aot/static/static-prop-dynamic-call-stable.phpt @@ -0,0 +1,27 @@ +--TEST-- +Static property local slots survive dynamic PHP calls +--FILE-- + +--EXPECT-- +int(100) +string(8) "changed!"