Skip to content

Commit 49861be

Browse files
SanderMullerclaude
authored andcommitted
Keep the expression result alive in the native ReturnHandler
The native processStmt() read the throw points, impure points and dependencies of the returned expression as pointers into its ExpressionResult. The result was released at the end of the inner block, before the node callback ran. A rule that called getType() from that callback could then reuse the freed memory, and the statement result got an unrelated object as its impure points. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent fe9a9b4 commit 49861be

4 files changed

Lines changed: 93 additions & 1 deletion

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt\ClassMethod;
7+
use PhpParser\Node\Stmt\Return_;
8+
use PhpParser\NodeFinder;
9+
use PHPStan\Rules\Rule;
10+
11+
/**
12+
* Asks the type of a method's returned expression from the callback for the
13+
* method itself, after the walk of the body is complete. The test only checks
14+
* that this does not crash.
15+
*
16+
* @implements Rule<ClassMethod>
17+
*/
18+
class ReturnedExprGetTypeRule implements Rule
19+
{
20+
21+
public function getNodeType(): string
22+
{
23+
return ClassMethod::class;
24+
}
25+
26+
public function processNode(Node $node, Scope $scope): array
27+
{
28+
$return = (new NodeFinder())->findFirstInstanceOf((array) $node->stmts, Return_::class);
29+
if ($return === null || $return->expr === null) {
30+
return [];
31+
}
32+
33+
$scope->getType($return->expr);
34+
35+
return [];
36+
}
37+
38+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\RequiresPhp;
8+
9+
/**
10+
* @extends RuleTestCase<ReturnedExprGetTypeRule>
11+
*/
12+
class ReturnedExprGetTypeRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ReturnedExprGetTypeRule();
18+
}
19+
20+
#[RequiresPhp('>= 8.0.0')]
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/returned-expr-get-type.php'], []);
24+
}
25+
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace ReturnedExprGetType;
6+
7+
/** @template T */
8+
final class Bag {
9+
/** @param list<T> $items */
10+
public function __construct(private array $items) {}
11+
/** @return list<T> */
12+
public function all(): array { return $this->items; }
13+
}
14+
15+
/**
16+
* @template TReturnType
17+
* @param callable(): TReturnType $callback
18+
* @return TReturnType
19+
*/
20+
function once4(callable $callback) { return $callback(); }
21+
22+
final class Box4 {
23+
/** @return Bag<string> */
24+
public function items(): Bag {
25+
return once4(function (): Bag { return new Bag(['a']); });
26+
}
27+
}

‎turbo-ext/src/ReturnHandler.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class ReturnHandler
7979
zval *throwPoints = &emptyArray;
8080
zval *impurePoints = &emptyArray;
8181
zval *dependencies = NULL;
82+
zv::Val result;
8283
zv::Val variableFlow;
8384
expr = returnedExpr(stmt);
8485
if (UNEXPECTED(expr == NULL)) return zv::Val();
@@ -96,7 +97,7 @@ class ReturnHandler
9697
Z_TYPE_P(expectedReturnType.raw()) == IS_OBJECT ? expectedReturnType.raw() : NULL,
9798
Z_TYPE_P(nativeExpectedReturnType.raw()) == IS_OBJECT ? nativeExpectedReturnType.raw() : NULL);
9899
if (UNEXPECTED(expressionContext.isUndef())) return zv::Val();
99-
zv::Val result = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, expr, stmtScope.raw(), storage, nodeCallback, expressionContext.raw());
100+
result = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, expr, stmtScope.raw(), storage, nodeCallback, expressionContext.raw());
100101
if (UNEXPECTED(result.isUndef())) return zv::Val();
101102
dependencies = pt_expression_result_dependencies(result.raw(), dependenciesHold);
102103
if (UNEXPECTED(dependencies == NULL)) return zv::Val();

0 commit comments

Comments
 (0)