From d39514338fba728ee8a9ce940a13e168b469de1b Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Mon, 26 Oct 2026 00:06:53 +0100 Subject: [PATCH] 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) --- .../Analyser/ReturnedExprGetTypeRule.php | 38 +++++++++++++++++++ .../Analyser/ReturnedExprGetTypeRuleTest.php | 26 +++++++++++++ .../Analyser/data/returned-expr-get-type.php | 27 +++++++++++++ turbo-ext/src/ReturnHandler.cpp | 3 +- 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/ReturnedExprGetTypeRule.php create mode 100644 tests/PHPStan/Analyser/ReturnedExprGetTypeRuleTest.php create mode 100644 tests/PHPStan/Analyser/data/returned-expr-get-type.php diff --git a/tests/PHPStan/Analyser/ReturnedExprGetTypeRule.php b/tests/PHPStan/Analyser/ReturnedExprGetTypeRule.php new file mode 100644 index 00000000000..bd5295a32d7 --- /dev/null +++ b/tests/PHPStan/Analyser/ReturnedExprGetTypeRule.php @@ -0,0 +1,38 @@ + + */ +class ReturnedExprGetTypeRule implements Rule +{ + + public function getNodeType(): string + { + return ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $return = (new NodeFinder())->findFirstInstanceOf((array) $node->stmts, Return_::class); + if ($return === null || $return->expr === null) { + return []; + } + + $scope->getType($return->expr); + + return []; + } + +} diff --git a/tests/PHPStan/Analyser/ReturnedExprGetTypeRuleTest.php b/tests/PHPStan/Analyser/ReturnedExprGetTypeRuleTest.php new file mode 100644 index 00000000000..666fe8e194a --- /dev/null +++ b/tests/PHPStan/Analyser/ReturnedExprGetTypeRuleTest.php @@ -0,0 +1,26 @@ + + */ +class ReturnedExprGetTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReturnedExprGetTypeRule(); + } + + #[RequiresPhp('>= 8.0.0')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/returned-expr-get-type.php'], []); + } + +} diff --git a/tests/PHPStan/Analyser/data/returned-expr-get-type.php b/tests/PHPStan/Analyser/data/returned-expr-get-type.php new file mode 100644 index 00000000000..301a5feefd7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/returned-expr-get-type.php @@ -0,0 +1,27 @@ += 8.0 + +declare(strict_types = 1); + +namespace ReturnedExprGetType; + +/** @template T */ +final class Bag { + /** @param list $items */ + public function __construct(private array $items) {} + /** @return list */ + public function all(): array { return $this->items; } +} + +/** + * @template TReturnType + * @param callable(): TReturnType $callback + * @return TReturnType + */ +function once4(callable $callback) { return $callback(); } + +final class Box4 { + /** @return Bag */ + public function items(): Bag { + return once4(function (): Bag { return new Bag(['a']); }); + } +} diff --git a/turbo-ext/src/ReturnHandler.cpp b/turbo-ext/src/ReturnHandler.cpp index f88043fb621..2a3f3c19788 100644 --- a/turbo-ext/src/ReturnHandler.cpp +++ b/turbo-ext/src/ReturnHandler.cpp @@ -79,6 +79,7 @@ class ReturnHandler zval *throwPoints = &emptyArray; zval *impurePoints = &emptyArray; zval *dependencies = NULL; + zv::Val result; zv::Val variableFlow; expr = returnedExpr(stmt); if (UNEXPECTED(expr == NULL)) return zv::Val(); @@ -96,7 +97,7 @@ class ReturnHandler Z_TYPE_P(expectedReturnType.raw()) == IS_OBJECT ? expectedReturnType.raw() : NULL, Z_TYPE_P(nativeExpectedReturnType.raw()) == IS_OBJECT ? nativeExpectedReturnType.raw() : NULL); if (UNEXPECTED(expressionContext.isUndef())) return zv::Val(); - zv::Val result = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, expr, stmtScope.raw(), storage, nodeCallback, expressionContext.raw()); + result = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, expr, stmtScope.raw(), storage, nodeCallback, expressionContext.raw()); if (UNEXPECTED(result.isUndef())) return zv::Val(); dependencies = pt_expression_result_dependencies(result.raw(), dependenciesHold); if (UNEXPECTED(dependencies == NULL)) return zv::Val();