diff --git a/lib/VM.php b/lib/VM.php index f42ace23e3e..e0f4e4c082a 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -16945,9 +16945,16 @@ protected function inheritFromParent(ClassEntry $entry): void $exists = true; break; } - if (!$exists) { - $entry->properties[] = $property; + if ($exists) { + // Child redeclared a non-private parent final property (#22988, Zend/zend_inheritance.c). + // Same-script compile is covered by FinalPropertyOverrideCheck; cross-eval needs + // this runtime path (see final class const #22329). + if (!$isPrivate && $property->propertyFinal) { + $this->rejectChildOverrideOfFinalProperty($entry, $property); + } + continue; } + $entry->properties[] = $property; } } @@ -18742,6 +18749,31 @@ private function rejectChildOverrideOfFinalClassConst( )); } + /** + * php-src zend_inheritance.c — "Cannot override final property %s::$%s" (#22988, #22339). + */ + private function rejectChildOverrideOfFinalProperty( + ClassEntry $entry, + VM\ClassProperty $parentProperty + ): void { + if (!$parentProperty->propertyFinal) { + return; + } + $declaringLc = $parentProperty->declaringClassLc; + $ownerDisplay = $declaringLc !== '' ? $declaringLc : $entry->parentLc; + if (is_string($ownerDisplay) && isset($this->context->classes[$ownerDisplay])) { + $ownerDisplay = $this->context->classes[$ownerDisplay]->name; + } + if (!is_string($ownerDisplay) || '' === $ownerDisplay) { + $ownerDisplay = 'parent'; + } + throw new \CompileError(sprintf( + 'Cannot override final property %s::$%s', + $ownerDisplay, + $parentProperty->name + )); + } + /** * Class body constant after trait use must not redefine an inherited trait constant * with an incompatible value (Zend/zend_traits.c zend_traits_compile_role_constants, #7012). diff --git a/test/compliance/JITTest.php b/test/compliance/JITTest.php index a68eafb574a..517a85ab1ca 100755 --- a/test/compliance/JITTest.php +++ b/test/compliance/JITTest.php @@ -1422,6 +1422,10 @@ public static function providePHPTests(): \Generator if (str_contains($name, 'final_class_const_eval_override')) { continue; } + // eval() final plain property override: VM inheritFromParent (#22988); MCJIT inline eval deferral pending. + if (str_contains($name, 'final_plain_property_eval_override')) { + continue; + } // preserve_keys=true: VM + JIT/AOT via ArrayBuiltinHelper (#3524). // array_merge_recursive(): VM + JIT via ArrayMergeRecursiveJitHelper PHP (#3297, #10183). if (str_contains(strtolower($case[0]), 'array_merge_recursive')) { diff --git a/test/compliance/cases/language/final_plain_property_eval_override_fatal.phpt b/test/compliance/cases/language/final_plain_property_eval_override_fatal.phpt new file mode 100644 index 00000000000..ee5d2a37ab6 --- /dev/null +++ b/test/compliance/cases/language/final_plain_property_eval_override_fatal.phpt @@ -0,0 +1,19 @@ +--TEST-- +Language: eval() cannot override final plain property (#22988, Zend/zend_inheritance.c) +--ENV-- +PHP_COMPILER_PROFILE=8.4 +--FILE-- +isFinal()) { + echo "not-final\n"; +} +eval('class B extends A { public string $x = "b"; }'); +echo "EVAL_OK\n"; +--EXPECTF-- +PHP Fatal error: Cannot override final property A::$x in %s : eval()'d code on line %d +--EXPECT_EXIT-- +255 diff --git a/test/repro/issue_22988_final_plain_property_eval_override.php b/test/repro/issue_22988_final_plain_property_eval_override.php new file mode 100644 index 00000000000..c9dc4dd5844 --- /dev/null +++ b/test/repro/issue_22988_final_plain_property_eval_override.php @@ -0,0 +1,8 @@ +x, "\n"; diff --git a/test/unit/FinalPropertyOverrideCheckTest.php b/test/unit/FinalPropertyOverrideCheckTest.php index ad8ef85e472..10fba5f48e2 100644 --- a/test/unit/FinalPropertyOverrideCheckTest.php +++ b/test/unit/FinalPropertyOverrideCheckTest.php @@ -96,6 +96,25 @@ class C { self::assertSame('ok', ob_get_clean()); } + /** @covers issue #22988 — cross-eval must hit inheritFromParent, not only same-script FinalPropertyOverrideCheck */ + public function testEvalCannotOverrideFinalPlainProperty(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +parseAndCompile($code, 'final_plain_eval_override.php'); + $this->assertNotNull($block); + $this->expectException(\CompileError::class); + $this->expectExceptionMessage('Cannot override final property A::$x'); + $runtime->run($block); + } + /** @covers issue #22474 */ public function testChildCannotOverrideFinalSetHook(): void {