Skip to content

RFC: Locked Classes #3931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0);
return 0;
}
if ((flags & ZEND_ACC_LOCKED) && (new_flag & ZEND_ACC_LOCKED)) {
zend_throw_exception(zend_ce_compile_error, "Multiple locked modifiers are not allowed", 0);
return 0;
}
if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) {
zend_throw_exception(zend_ce_compile_error,
"Cannot use the final modifier on an abstract class", 0);
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ typedef struct _zend_oparray_context {
/* Top-level class or function declaration | | | */
#define ZEND_ACC_TOP_LEVEL (1 << 9) /* X | X | | */
/* | | | */
/* Class Flags (unused: 16...) | | | */
/* Class Flags (unused: 17...) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
Expand Down Expand Up @@ -266,6 +266,9 @@ typedef struct _zend_oparray_context {
/* Children must reuse parent get_iterator() | | | */
#define ZEND_ACC_REUSE_GET_ITERATOR (1 << 17) /* X | | | */
/* | | | */
/* Class is declared "locked" | | | */
#define ZEND_ACC_LOCKED (1 << 18) /* X | | | */
Copy link
Contributor Author

@IMSoP IMSoP Mar 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this constant will be exposed to userland (via ReflectionClass::IS_LOCKED), whereas most of these aren't, such a high number might seem surprising to users, but I'm not sure of the impact of rearranging this list.

Copy link
Member

@nikic nikic Mar 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't make any particularly guarantees about the values of those flags in reflection (in fact, they change between releases). I wouldn't worry about this.

/* | | | */
/* Function Flags (unused: 28...30) | | | */
/* ============== | | | */
/* | | | */
Expand Down
6 changes: 4 additions & 2 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%left T_ELSEIF
%left T_ELSE
%left T_ENDIF
%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC
%right T_STATIC T_ABSTRACT T_FINAL T_LOCKED T_PRIVATE T_PROTECTED T_PUBLIC

%token <ast> T_LNUMBER "integer number (T_LNUMBER)"
%token <ast> T_DNUMBER "floating-point number (T_DNUMBER)"
Expand Down Expand Up @@ -178,6 +178,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%token T_STATIC "static (T_STATIC)"
%token T_ABSTRACT "abstract (T_ABSTRACT)"
%token T_FINAL "final (T_FINAL)"
%token T_LOCKED "locked (T_LOCKED)"
%token T_PRIVATE "private (T_PRIVATE)"
%token T_PROTECTED "protected (T_PROTECTED)"
%token T_PUBLIC "public (T_PUBLIC)"
Expand Down Expand Up @@ -278,7 +279,7 @@ reserved_non_modifiers:

semi_reserved:
reserved_non_modifiers
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC
| T_STATIC | T_ABSTRACT | T_FINAL | T_LOCKED | T_PRIVATE | T_PROTECTED | T_PUBLIC
;

identifier:
Expand Down Expand Up @@ -518,6 +519,7 @@ class_modifiers:
class_modifier:
T_ABSTRACT { $$ = ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; }
| T_FINAL { $$ = ZEND_ACC_FINAL; }
| T_LOCKED { $$ = ZEND_ACC_LOCKED; }
;

trait_declaration_statement:
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_language_scanner.l
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,10 @@ NEWLINE ("\r"|"\n"|"\r\n")
RETURN_TOKEN(T_FINAL);
}

<ST_IN_SCRIPTING>"locked" {
RETURN_TOKEN(T_LOCKED);
}

<ST_IN_SCRIPTING>"private" {
RETURN_TOKEN(T_PRIVATE);
}
Expand Down
17 changes: 17 additions & 0 deletions Zend/zend_object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,8 @@ ZEND_API zval *zend_std_read_property(zval *object, zval *member, int type, void
zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization",
ZSTR_VAL(prop_info->ce->name),
ZSTR_VAL(name));
} else if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) {
zend_throw_error(NULL, "Cannot access undefined property $%s on locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name));
} else {
zend_error(E_NOTICE,"Undefined property: %s::$%s", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name));
}
Expand Down Expand Up @@ -883,6 +885,11 @@ ZEND_API zval *zend_std_write_property(zval *object, zval *member, zval *value,

ZVAL_COPY_VALUE(variable_ptr, value);
} else {
if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) {
zend_throw_error(NULL, "Cannot write undefined property $%s on locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name));
variable_ptr = &EG(error_zval);
goto exit;
}
if (!zobj->properties) {
rebuild_object_properties(zobj);
}
Expand Down Expand Up @@ -1075,6 +1082,11 @@ ZEND_API void zend_std_unset_property(zval *object, zval *member, void **cache_s
property_offset = zend_get_property_offset(zobj->ce, name, (zobj->ce->__unset != NULL), cache_slot, &prop_info);

if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) {
if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) {
zend_throw_error(NULL, "Cannot unset property $%s of locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name));
goto exit;
}

zval *slot = OBJ_PROP(zobj, property_offset);

if (Z_TYPE_P(slot) != IS_UNDEF) {
Expand All @@ -1093,6 +1105,11 @@ ZEND_API void zend_std_unset_property(zval *object, zval *member, void **cache_s
}
} else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))
&& EXPECTED(zobj->properties != NULL)) {
if ( zobj->ce->ce_flags & ZEND_ACC_LOCKED ) {
zend_throw_error(NULL, "Cannot unset property $%s of locked class %s", ZSTR_VAL(name), ZSTR_VAL(zobj->ce->name));
goto exit;
}

if (UNEXPECTED(GC_REFCOUNT(zobj->properties) > 1)) {
if (EXPECTED(!(GC_FLAGS(zobj->properties) & IS_ARRAY_IMMUTABLE))) {
GC_DELREF(zobj->properties);
Expand Down
19 changes: 18 additions & 1 deletion ext/reflection/php_reflection.c
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ static void _class_string(smart_str *str, zend_class_entry *ce, zval *obj, char
if (ce->ce_flags & ZEND_ACC_FINAL) {
smart_str_append_printf(str, "final ");
}
if (ce->ce_flags & ZEND_ACC_LOCKED) {
smart_str_append_printf(str, "locked ");
}
smart_str_append_printf(str, "class ");
}
smart_str_append_printf(str, "%s", ZSTR_VAL(ce->name));
Expand Down Expand Up @@ -1478,6 +1481,9 @@ ZEND_METHOD(reflection, getModifierNames)
if (modifiers & ZEND_ACC_FINAL) {
add_next_index_stringl(return_value, "final", sizeof("final")-1);
}
if (modifiers & ZEND_ACC_LOCKED) {
add_next_index_stringl(return_value, "locked", sizeof("locked")-1);
}

/* These are mutually exclusive */
switch (modifiers & ZEND_ACC_PPP_MASK) {
Expand Down Expand Up @@ -4579,14 +4585,23 @@ ZEND_METHOD(reflection_class, isAbstract)
}
/* }}} */

/* {{{ proto public bool ReflectionClass::isLocked()
Returns whether this class is locked */
ZEND_METHOD(reflection_class, isLocked)
{
_class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_LOCKED);
}
/* }}} */

/* {{{ proto public int ReflectionClass::getModifiers()
Returns a bitfield of the access modifiers for this class */
ZEND_METHOD(reflection_class, getModifiers)
{
reflection_object *intern;
zend_class_entry *ce;
uint32_t keep_flags = ZEND_ACC_FINAL
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS;
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS
| ZEND_ACC_LOCKED;

if (zend_parse_parameters_none() == FAILURE) {
return;
Expand Down Expand Up @@ -6513,6 +6528,7 @@ static const zend_function_entry reflection_class_functions[] = {
ZEND_ME(reflection_class, isTrait, arginfo_reflection__void, 0)
ZEND_ME(reflection_class, isAbstract, arginfo_reflection__void, 0)
ZEND_ME(reflection_class, isFinal, arginfo_reflection__void, 0)
ZEND_ME(reflection_class, isLocked, arginfo_reflection__void, 0)
ZEND_ME(reflection_class, getModifiers, arginfo_reflection__void, 0)
ZEND_ME(reflection_class, isInstance, arginfo_reflection_class_isInstance, 0)
ZEND_ME(reflection_class, newInstance, arginfo_reflection_class_newInstance, 0)
Expand Down Expand Up @@ -6846,6 +6862,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_LOCKED", ZEND_ACC_LOCKED);

INIT_CLASS_ENTRY(_reflection_entry, "ReflectionObject", reflection_object_functions);
reflection_init_class_handlers(&_reflection_entry);
Expand Down
11 changes: 9 additions & 2 deletions ext/reflection/tests/ReflectionClass_toString_001.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ echo $rc;
--EXPECT--
Class [ <internal:Reflection> class ReflectionClass implements Reflector ] {

- Constants [3] {
- Constants [4] {
Constant [ public int IS_IMPLICIT_ABSTRACT ] { 16 }
Constant [ public int IS_EXPLICIT_ABSTRACT ] { 64 }
Constant [ public int IS_FINAL ] { 32 }
Constant [ public int IS_LOCKED ] { 262144 }
}

- Static properties [0] {
Expand All @@ -34,7 +35,7 @@ Class [ <internal:Reflection> class ReflectionClass implements Reflector ] {
Property [ <default> public $name ]
}

- Methods [53] {
- Methods [54] {
Method [ <internal:Reflection> final private method __clone ] {

- Parameters [0] {
Expand Down Expand Up @@ -249,6 +250,12 @@ Class [ <internal:Reflection> class ReflectionClass implements Reflector ] {
}
}

Method [ <internal:Reflection> public method isLocked ] {

- Parameters [0] {
}
}

Method [ <internal:Reflection> public method getModifiers ] {

- Parameters [0] {
Expand Down
40 changes: 40 additions & 0 deletions tests/classes/class_locked_derived.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
A sub-class of a locked class is not locked unless explciitly marked

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A sub-class of a locked class is not locked unless explciitly marked
A sub-class of a locked class is not locked unless explicitly marked

--FILE--
<?php

locked class Base {
public $definedProp;
}
class UnlockedDerived extends Base {
}
locked class LockedDerived extends Base {
}

$t = new UnlockedDerived();

$t->definedProp = "OK\n";
echo $t->definedProp;
unset($t->definedProp);

$t->nonExistentProp = "Also OK\n";
echo $t->nonExistentProp;
unset($t->nonExistentProp);


$t = new LockedDerived();

$t->definedProp = "OK\n";
$t->nonExistentProp = "Not OK\n";


echo "Done\n"; // shouldn't be displayed
?>
--EXPECTF--
OK
Also OK

Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
25 changes: 25 additions & 0 deletions tests/classes/class_locked_get.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
A locked class prevents undefined properties being accessed
--FILE--
<?php

locked class TestClass {
public $definedProp;
}

$t = new testClass();

$t->definedProp = "OK\n";
echo $t->definedProp;

echo $t->nonExistentProp;

echo "Done\n"; // shouldn't be displayed
?>
--EXPECTF--
OK

Fatal error: Uncaught Error: Cannot access undefined property %s on locked class %s in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
30 changes: 30 additions & 0 deletions tests/classes/class_locked_get_overload.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
A locked class may still have __get overload
--FILE--
<?php

locked class TestClass {
public $definedProp;
private $privateProp;

public function __get($prop) {
return "__get called for $prop\n";
}
}

$t = new testClass();

$t->definedProp = "OK\n";
echo $t->definedProp;

echo $t->privateProp;
echo $t->nonExistentProp;

echo "Done\n";
?>
--EXPECT--
OK
__get called for privateProp
__get called for nonExistentProp
Done

29 changes: 29 additions & 0 deletions tests/classes/class_locked_private.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
A locked class cannot set undefined properties on its own instances
--FILE--
<?php

locked class TestClass {
private $definedProp;

public function test() {
$this->definedProp = "OK\n";
echo $this->definedProp;

$this->nonExistentProp = "Not OK\n";
}
}

$t = new testClass();
$t->test();

echo "Done\n"; // shouldn't be displayed
?>
--EXPECTF--
OK

Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d
Stack trace:
#0 %s(%d): TestClass->test()
#1 {main}
thrown in %s on line %d
25 changes: 25 additions & 0 deletions tests/classes/class_locked_set.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
A locked class prevents undefined properties being set
--FILE--
<?php

locked class TestClass {
public $definedProp;
}

$t = new testClass();

$t->definedProp = "OK\n";
echo $t->definedProp;

$t->nonExistentProp = "Not OK\n";

echo "Done\n"; // shouldn't be displayed
?>
--EXPECTF--
OK

Fatal error: Uncaught Error: Cannot write undefined property %s on locked class %s in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
Loading