Fix GH-19044: Protected properties are not scoped according to their prototype (#19046)

* Fix GH-19044: Protected properties are not scoped according to their prototype

* Adjust after review

* Simplify to using prototype even for asymmetric visibility
This commit is contained in:
Bob Weinand 2025-07-22 17:46:14 +02:00 committed by GitHub
parent 4e1d3f8772
commit b13347be38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 243 additions and 8 deletions

5
NEWS
View file

@ -7,6 +7,8 @@ PHP NEWS
(psumbera)
. Fixed bug GH-19053 (Duplicate property slot with hooks and interface
property). (ilutov)
. Fixed bug GH-19044 (Protected properties are not scoped according to their
prototype). (Bob)
- Hash:
. Fix crash on clone failure. (nielsdos)
@ -49,9 +51,6 @@ PHP NEWS
. Fixed bug GH-18907 (Leak when creating cycle in hook). (ilutov)
. Fix OSS-Fuzz #427814456. (nielsdos)
. Fix OSS-Fuzz #428983568 and #428760800. (nielsdos)
. Fixed bug GH-17204 (-Wuseless-escape warnings emitted by re2c). (Peter Kokot)
. Fixed bug GH-19064 (Undefined symbol 'execute_ex' on Windows ARM64).
(Demon)
- Curl:
. Fix memory leaks when returning refcounted value from curl callback.

View file

@ -0,0 +1,24 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (protected(set) on non-hooked property)
--FILE--
<?php
class P {
public mixed $foo { get => 42; }
}
class C1 extends P {
public protected(set) mixed $foo = 1;
}
class C2 extends P {
public protected(set) mixed $foo;
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(43)

26
Zend/tests/gh19044.phpt Normal file
View file

@ -0,0 +1,26 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype
--FILE--
<?php
abstract class P {
protected $foo;
}
class C1 extends P {
protected $foo = 1;
}
class C2 extends P {
protected $foo = 2;
static function foo($c) { return $c->foo; }
}
var_dump(C2::foo(new C2));
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(2)
int(1)

View file

@ -0,0 +1,26 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (common ancestor has a protected setter)
--FILE--
<?php
abstract class P {
abstract public mixed $foo { get; }
}
class C1 extends P {
public protected(set) mixed $foo { get => 1; set {} }
}
class GrandC1 extends C1 {
public protected(set) mixed $foo { get => 2; set {} }
}
class C2 extends C1 {
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new GrandC1));
?>
--EXPECT--
int(3)

View file

@ -0,0 +1,26 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (common ancestor does not have a setter)
--FILE--
<?php
abstract class P {
abstract public mixed $foo { get; }
}
class C1 extends P {
public mixed $foo { get => 1; }
}
class GrandC1 extends C1 {
public protected(set) mixed $foo { get => 2; set {} }
}
class C2 extends C1 {
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new GrandC1));
?>
--EXPECT--
int(3)

View file

@ -0,0 +1,24 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (abstract parent defining visibility only takes precedence)
--FILE--
<?php
abstract class P {
abstract protected(set) mixed $foo { get; set; }
}
class C1 extends P {
public protected(set) mixed $foo { get => 2; set {} }
}
class C2 extends P {
public mixed $foo = 1;
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(3)

View file

@ -0,0 +1,28 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (abstract parent sets protected(set) with not having grandparent a setter - both inherit from parent)
--FILE--
<?php
abstract class GP {
abstract mixed $foo { get; }
}
abstract class P extends GP {
abstract protected(set) mixed $foo { get; set; }
}
class C1 extends P {
public protected(set) mixed $foo { get => 2; set {} }
}
class C2 extends P {
public mixed $foo = 1;
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(3)

View file

@ -0,0 +1,28 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (abstract parent sets protected(set) with not having grandparent a setter - one inherits from grandparent)
--FILE--
<?php
abstract class GP {
abstract mixed $foo { get; }
}
abstract class P extends GP {
abstract protected(set) mixed $foo { get; set; }
}
class C1 extends P {
public protected(set) mixed $foo { get => 2; set {} }
}
class C2 extends GP {
public mixed $foo = 1;
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(3)

View file

@ -0,0 +1,28 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (abstract parent has implicit set hook)
--FILE--
<?php
abstract class GP {
public abstract mixed $foo { get; }
}
class P extends GP {
public protected(set) mixed $foo { get => $this->foo; }
}
class C1 extends P {
public protected(set) mixed $foo = 1;
}
class C2 extends P {
public protected(set) mixed $foo;
static function foo($c) { return $c->foo += 1; }
}
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(2)

View file

@ -0,0 +1,26 @@
--TEST--
GH-19044: Protected properties must be scoped according to their prototype (hooks variation)
--FILE--
<?php
abstract class P {
abstract protected $foo { get; }
}
class C1 extends P {
protected $foo = 1;
}
class C2 extends P {
protected $foo = 2;
static function foo($c) { return $c->foo; }
}
var_dump(C2::foo(new C2));
var_dump(C2::foo(new C1));
?>
--EXPECT--
int(2)
int(1)

View file

@ -282,7 +282,7 @@ static zend_always_inline bool is_derived_class(const zend_class_entry *child_cl
static zend_never_inline int is_protected_compatible_scope(const zend_class_entry *ce, const zend_class_entry *scope) /* {{{ */
{
return scope &&
(is_derived_class(ce, scope) || is_derived_class(scope, ce));
(ce == scope || is_derived_class(ce, scope) || is_derived_class(scope, ce));
}
/* }}} */
@ -419,7 +419,7 @@ wrong:
}
} else {
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
if (UNEXPECTED(!is_protected_compatible_scope(property_info->ce, scope))) {
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
goto wrong;
}
}
@ -514,7 +514,7 @@ wrong:
}
} else {
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
if (UNEXPECTED(!is_protected_compatible_scope(property_info->ce, scope))) {
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
goto wrong;
}
}
@ -585,7 +585,7 @@ ZEND_API bool ZEND_FASTCALL zend_asymmetric_property_has_set_access(const zend_p
return true;
}
return EXPECTED((prop_info->flags & ZEND_ACC_PROTECTED_SET)
&& is_protected_compatible_scope(prop_info->ce, scope));
&& is_protected_compatible_scope(prop_info->prototype->ce, scope));
}
static void zend_property_guard_dtor(zval *el) /* {{{ */ {
@ -2030,7 +2030,7 @@ ZEND_API zval *zend_std_get_static_property_with_info(zend_class_entry *ce, zend
zend_class_entry *scope = get_fake_or_executed_scope();
if (property_info->ce != scope) {
if (UNEXPECTED(property_info->flags & ZEND_ACC_PRIVATE)
|| UNEXPECTED(!is_protected_compatible_scope(property_info->ce, scope))) {
|| UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
if (type != BP_VAR_IS) {
zend_bad_property_access(property_info, ce, property_name);
}