fix(optimizer): 解决对象属性优化中的动态调用安全检查问题

- 添加了对 eval 和 include 语句的对象属性访问保护
- 实现了内部函数和静态方法调用的安全性检查机制
- 新增了 isSafeObjectExposureCall 方法来判断安全的对象暴露调用
- 修复了对象别名和动态属性操作中的潜在风险
- 添加了相关的单元测试验证安全性检查逻辑
pull/1/head
韩天峰 3 months ago
parent 4757bd353c
commit ba8c96712a
  1. 56
      phpunit/src/SsaAnalysisTest.php
  2. 75
      src/Php/Optimizer/SsaPropOptimizer.php
  3. 8
      tests/aot/optimizations/objprop-hoist-this-object-arg-escape.phpt
  4. 27
      tests/aot/static/static-prop-dynamic-call-stable.phpt

@ -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'));

@ -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.

@ -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();
}

@ -0,0 +1,27 @@
--TEST--
Static property local slots survive dynamic PHP calls
--FILE--
<?php
use native_types;
class StaticDynamicCallStable {
public static int $i = 1;
public static string $s = "seed";
}
function main(): void {
StaticDynamicCallStable::$i = 10;
StaticDynamicCallStable::$s = "before";
eval('function mutate_static(): void { StaticDynamicCallStable::$i = 99; StaticDynamicCallStable::$s = "changed"; } mutate_static();');
StaticDynamicCallStable::$i += 1;
StaticDynamicCallStable::$s .= "!";
var_dump(StaticDynamicCallStable::$i);
var_dump(StaticDynamicCallStable::$s);
}
?>
--EXPECT--
int(100)
string(8) "changed!"
Loading…
Cancel
Save