Skip to content

Commit 9fd5013

Browse files
committed
Fix critical bug and implement property/method namespace visibility
This commit fixes a critical bug where ZEND_ACC_NAMESPACE_PRIVATE was stored at bit 30, exceeding the 16-bit AST attr field limit. It also completes the runtime enforcement of namespace visibility for both properties and methods. Critical Bug Fix: - Move ZEND_ACC_NAMESPACE_PRIVATE from bit 30 to bit 14 - AST nodes use uint16_t for attr field (max bit 15) - Previous bit 30 caused flag truncation to 0x0000 during parsing - Now fits properly in 16-bit field as 0x4000 Property Visibility Implementation: - Add namespace-based name mangling in zend_declare_typed_property() - Mangle private(namespace) properties with namespace prefix - Bypass property offset cache for namespace_private properties (visibility depends on caller's runtime namespace context) - Move namespace visibility check outside scope comparison (applies to all classes in namespace, not just inheritance chain) - Separate namespace_private from private/protected checks Method Visibility Implementation: - Exclude namespace_private methods from private/protected checks - Add namespace visibility validation in zend_std_get_method() - Check caller namespace matches method's class namespace
1 parent cfc676b commit 9fd5013

File tree

3 files changed

+78
-51
lines changed

3 files changed

+78
-51
lines changed

Zend/zend_API.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4603,6 +4603,10 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
46034603
property_info->name = zend_string_copy(name);
46044604
} else if (access_type & ZEND_ACC_PRIVATE) {
46054605
property_info->name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
4606+
} else if (access_type & ZEND_ACC_NAMESPACE_PRIVATE) {
4607+
/* Mangle with namespace name to prevent external access while allowing same-namespace access */
4608+
zend_string *namespace = zend_get_class_namespace(ce);
4609+
property_info->name = zend_mangle_property_name(ZSTR_VAL(namespace), ZSTR_LEN(namespace), ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
46064610
} else {
46074611
ZEND_ASSERT(access_type & ZEND_ACC_PROTECTED);
46084612
property_info->name = zend_mangle_property_name("*", 1, ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
@@ -5403,7 +5407,8 @@ ZEND_API zend_string* zend_get_caller_namespace(void)
54035407
* For trait methods, scope is the class that uses the trait,
54045408
* not the trait itself. This is the desired behavior. */
54055409
if (ex->func->common.scope) {
5406-
return zend_get_class_namespace(ex->func->common.scope);
5410+
zend_string *ns = zend_get_class_namespace(ex->func->common.scope);
5411+
return ns;
54075412
}
54085413

54095414
/* Case 2: Called from a user function or top-level code */

Zend/zend_compile.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ typedef struct _zend_oparray_context {
222222
#define ZEND_ACC_PRIVATE (1 << 2) /* | X | X | X */
223223
/* | | | */
224224
/* Namespace-scoped visibility | | | */
225-
#define ZEND_ACC_NAMESPACE_PRIVATE (1 << 30) /* | X | X | */
225+
#define ZEND_ACC_NAMESPACE_PRIVATE (1 << 14) /* | X | X | */
226226
/* | | | */
227227
/* Property or method overrides private one | | | */
228228
#define ZEND_ACC_CHANGED (1 << 3) /* | X | X | */

Zend/zend_object_handlers.c

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,14 @@ static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *c
368368
uintptr_t offset;
369369

370370
if (cache_slot && EXPECTED(ce == CACHED_PTR_EX(cache_slot))) {
371-
*info_ptr = CACHED_PTR_EX(cache_slot + 2);
372-
return (uintptr_t)CACHED_PTR_EX(cache_slot + 1);
371+
const zend_property_info *cached_prop_info = CACHED_PTR_EX(cache_slot + 2);
372+
/* Disable caching for namespace_private properties since visibility depends on caller's namespace */
373+
if (UNEXPECTED(cached_prop_info && (cached_prop_info->flags & ZEND_ACC_NAMESPACE_PRIVATE))) {
374+
/* Fall through to do the visibility check */
375+
} else {
376+
*info_ptr = cached_prop_info;
377+
return (uintptr_t)CACHED_PTR_EX(cache_slot + 1);
378+
}
373379
}
374380

375381
if (UNEXPECTED(zend_hash_num_elements(&ce->properties_info) == 0)
@@ -391,6 +397,7 @@ static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *c
391397
property_info = (zend_property_info*)Z_PTR_P(zv);
392398
flags = property_info->flags;
393399

400+
394401
if (flags & (ZEND_ACC_CHANGED|ZEND_ACC_PRIVATE|ZEND_ACC_PROTECTED|ZEND_ACC_NAMESPACE_PRIVATE)) {
395402
const zend_class_entry *scope = get_fake_or_executed_scope();
396403

@@ -410,30 +417,36 @@ static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *c
410417
goto found;
411418
}
412419
}
413-
if (flags & ZEND_ACC_PRIVATE) {
414-
if (property_info->ce != ce) {
415-
goto dynamic;
416-
} else {
420+
/* Check private/protected, but not namespace_private (handled separately below) */
421+
if (!(flags & ZEND_ACC_NAMESPACE_PRIVATE)) {
422+
if (flags & ZEND_ACC_PRIVATE) {
423+
if (property_info->ce != ce) {
424+
goto dynamic;
425+
} else {
417426
wrong:
418-
/* Information was available, but we were denied access. Error out. */
419-
if (!silent) {
420-
zend_bad_property_access(property_info, ce, member);
427+
/* Information was available, but we were denied access. Error out. */
428+
if (!silent) {
429+
zend_bad_property_access(property_info, ce, member);
430+
}
431+
return ZEND_WRONG_PROPERTY_OFFSET;
432+
}
433+
} else {
434+
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
435+
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
436+
goto wrong;
421437
}
422-
return ZEND_WRONG_PROPERTY_OFFSET;
423438
}
424-
} else if (flags & ZEND_ACC_NAMESPACE_PRIVATE) {
425-
/* Check namespace visibility */
426-
zend_string *property_namespace = zend_get_class_namespace(property_info->ce);
427-
zend_string *caller_namespace = zend_get_caller_namespace();
439+
}
440+
}
428441

429-
if (!zend_string_equals(property_namespace, caller_namespace)) {
430-
goto wrong;
431-
}
432-
} else {
433-
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
434-
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
435-
goto wrong;
436-
}
442+
/* Check namespace visibility (must be outside scope check) */
443+
if (flags & ZEND_ACC_NAMESPACE_PRIVATE) {
444+
zend_string *property_namespace = zend_get_class_namespace(property_info->ce);
445+
zend_string *caller_namespace = zend_get_caller_namespace();
446+
447+
448+
if (!zend_string_equals(property_namespace, caller_namespace)) {
449+
goto wrong;
437450
}
438451
}
439452
}
@@ -513,30 +526,36 @@ ZEND_API zend_property_info *zend_get_property_info(const zend_class_entry *ce,
513526
goto found;
514527
}
515528
}
516-
if (flags & ZEND_ACC_PRIVATE) {
517-
if (property_info->ce != ce) {
518-
goto dynamic;
519-
} else {
529+
/* Check private/protected, but not namespace_private (handled separately below) */
530+
if (!(flags & ZEND_ACC_NAMESPACE_PRIVATE)) {
531+
if (flags & ZEND_ACC_PRIVATE) {
532+
if (property_info->ce != ce) {
533+
goto dynamic;
534+
} else {
520535
wrong:
521-
/* Information was available, but we were denied access. Error out. */
522-
if (!silent) {
523-
zend_bad_property_access(property_info, ce, member);
536+
/* Information was available, but we were denied access. Error out. */
537+
if (!silent) {
538+
zend_bad_property_access(property_info, ce, member);
539+
}
540+
return ZEND_WRONG_PROPERTY_INFO;
541+
}
542+
} else {
543+
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
544+
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
545+
goto wrong;
524546
}
525-
return ZEND_WRONG_PROPERTY_INFO;
526547
}
527-
} else if (flags & ZEND_ACC_NAMESPACE_PRIVATE) {
528-
/* Check namespace visibility */
529-
zend_string *property_namespace = zend_get_class_namespace(property_info->ce);
530-
zend_string *caller_namespace = zend_get_caller_namespace();
548+
}
549+
}
531550

532-
if (!zend_string_equals(property_namespace, caller_namespace)) {
533-
goto wrong;
534-
}
535-
} else {
536-
ZEND_ASSERT(flags & ZEND_ACC_PROTECTED);
537-
if (UNEXPECTED(!is_protected_compatible_scope(property_info->prototype->ce, scope))) {
538-
goto wrong;
539-
}
551+
/* Check namespace visibility (must be outside scope check) */
552+
if (flags & ZEND_ACC_NAMESPACE_PRIVATE) {
553+
zend_string *property_namespace = zend_get_class_namespace(property_info->ce);
554+
zend_string *caller_namespace = zend_get_caller_namespace();
555+
556+
557+
if (!zend_string_equals(property_namespace, caller_namespace)) {
558+
goto wrong;
540559
}
541560
}
542561
}
@@ -1913,13 +1932,16 @@ ZEND_API zend_function *zend_std_get_method(zend_object **obj_ptr, zend_string *
19131932
goto exit;
19141933
}
19151934
}
1916-
if (UNEXPECTED(fbc->op_array.fn_flags & ZEND_ACC_PRIVATE)
1917-
|| UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), scope))) {
1918-
if (zobj->ce->__call) {
1919-
fbc = zend_get_call_trampoline_func(zobj->ce->__call, method_name);
1920-
} else {
1921-
zend_bad_method_call(fbc, method_name, scope);
1922-
fbc = NULL;
1935+
/* Check private/protected, but not namespace_private (handled separately below) */
1936+
if (!(fbc->op_array.fn_flags & ZEND_ACC_NAMESPACE_PRIVATE)) {
1937+
if (UNEXPECTED(fbc->op_array.fn_flags & ZEND_ACC_PRIVATE)
1938+
|| UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), scope))) {
1939+
if (zobj->ce->__call) {
1940+
fbc = zend_get_call_trampoline_func(zobj->ce->__call, method_name);
1941+
} else {
1942+
zend_bad_method_call(fbc, method_name, scope);
1943+
fbc = NULL;
1944+
}
19231945
}
19241946
}
19251947
}

0 commit comments

Comments
 (0)