Skip to content
Open
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
85 changes: 85 additions & 0 deletions Zend/tests/assign_obj_ref_track_mutation_order.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
--TEST--
ASSIGN_OBJ_REF tracks object mutations before freeing operands
--SKIPIF--
<?php
$zendDir = dirname(__DIR__);
if (!is_file($zendDir . '/zend_vm_def.h') || !is_file($zendDir . '/zend_vm_execute.h')) {
die('skip source tree required');
}
?>
--FILE--
<?php

function assertAssignObjRefTrackingOrder(string $section, string $label): bool
{
$trackPos = strpos($section, 'ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj);');
$freeNeedles = [
'FREE_OP1();',
'FREE_OP2();',
'FREE_OP_DATA();',
'zval_ptr_dtor_nogc(EX_VAR(opline->op1.var));',
'zval_ptr_dtor_nogc(EX_VAR(opline->op2.var));',
'zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var));',
];
$freePositions = [];

foreach ($freeNeedles as $needle) {
$pos = strpos($section, $needle);
if ($pos !== false) {
$freePositions[] = $pos;
}
}

if ($trackPos === false) {
throw new RuntimeException($label . ' is missing the expected ASSIGN_OBJ_REF sequence');
}

if ($freePositions === []) {
return false;
}

if ($trackPos > min($freePositions)) {
throw new RuntimeException($label . ' tracks object mutation after freeing operands');
}

return true;
}

$zendDir = dirname(__DIR__);

$vmDef = file_get_contents($zendDir . '/zend_vm_def.h');
$defStart = strpos($vmDef, 'ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF');
$defEnd = strpos($vmDef, 'ZEND_VM_HANDLER(33, ZEND_ASSIGN_STATIC_PROP_REF');
if ($defStart === false || $defEnd === false) {
throw new RuntimeException('Unable to locate ZEND_ASSIGN_OBJ_REF in zend_vm_def.h');
}

if (!assertAssignObjRefTrackingOrder(substr($vmDef, $defStart, $defEnd - $defStart), 'zend_vm_def.h')) {
throw new RuntimeException('zend_vm_def.h ASSIGN_OBJ_REF unexpectedly has no operand frees');
}

$vmExecute = file_get_contents($zendDir . '/zend_vm_execute.h');
$sections = preg_split('/(?=static ZEND_OPCODE_HANDLER_RET )/', $vmExecute);
$checked = 0;

foreach ($sections as $section) {
$headerEnd = strpos($section, "\n");
$label = $headerEnd === false ? 'zend_vm_execute.h ASSIGN_OBJ_REF handler' : trim(substr($section, 0, $headerEnd));
if (!preg_match('/\bZEND_ASSIGN_OBJ_REF_[A-Z0-9_]+_HANDLER\b/', $label)) {
continue;
}

if (assertAssignObjRefTrackingOrder($section, $label)) {
$checked++;
}
}

if ($checked === 0) {
throw new RuntimeException('Unable to locate generated ZEND_ASSIGN_OBJ_REF handlers in zend_vm_execute.h');
}

echo "OK\n";

?>
--EXPECT--
OK
78 changes: 78 additions & 0 deletions Zend/tests/tracked_mutation_hook_safety.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
--TEST--
Tracked mutation hooks are called through defensive helpers
--SKIPIF--
<?php
$zendDir = dirname(__DIR__);
foreach (['zend_execute.h', 'zend_execute.c', 'zend_vm_def.h', 'zend_vm_execute.h'] as $file) {
if (!is_file($zendDir . '/' . $file)) {
die('skip source tree required');
}
}
?>
--FILE--
<?php

function sourceSection(string $source, string $start, string $end, string $label): string
{
$startPos = strpos($source, $start);
$endPos = strpos($source, $end, $startPos === false ? 0 : $startPos);

if ($startPos === false || $endPos === false || $endPos <= $startPos) {
throw new RuntimeException('Unable to locate ' . $label);
}

return substr($source, $startPos, $endPos - $startPos);
}

$zendDir = dirname(__DIR__);
$executeH = file_get_contents($zendDir . '/zend_execute.h');
$executeC = file_get_contents($zendDir . '/zend_execute.c');

if (!str_contains($executeH, 'static zend_always_inline bool zend_maybe_track_hash_mutation(HashTable *ht, bool publish)')) {
throw new RuntimeException('Missing hash mutation helper');
}

if (!str_contains($executeH, 'zend_tracked_hash_mutation_hook != NULL')) {
throw new RuntimeException('Hash mutation helper does not guard the hook pointer');
}

foreach (['zend_execute.c', 'zend_vm_def.h', 'zend_vm_execute.h'] as $file) {
$source = file_get_contents($zendDir . '/' . $file);
if (str_contains($source, 'zend_tracked_hash_mutation_hook(')) {
throw new RuntimeException($file . ' calls zend_tracked_hash_mutation_hook() directly');
}
}

if (!str_contains($executeC, 'zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL')) {
throw new RuntimeException('Object mutation slow path does not guard the hook pointer');
}

if (!str_contains($executeC, 'zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL && value != &EG(error_zval)')) {
throw new RuntimeException('Object mutation with-value slow path does not guard the hook pointer');
}

$assign = sourceSection(
$executeH,
'static zend_always_inline zval* zend_assign_to_variable(',
'static zend_always_inline zval* zend_assign_to_variable_ex(',
'zend_assign_to_variable()'
);
if (substr_count($assign, 'zend_maybe_track_reference_update(updated_ref);') < 2) {
throw new RuntimeException('zend_assign_to_variable() does not notify reference updates');
}

$assignEx = sourceSection(
$executeH,
'static zend_always_inline zval* zend_assign_to_variable_ex(',
'static zend_always_inline void zend_class_static_update(',
'zend_assign_to_variable_ex()'
);
if (substr_count($assignEx, 'zend_maybe_track_reference_update(updated_ref);') < 2) {
throw new RuntimeException('zend_assign_to_variable_ex() does not notify reference updates');
}

echo "OK\n";

?>
--EXPECT--
OK
9 changes: 9 additions & 0 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ ZEND_API zend_string *(*zend_resolve_path)(zend_string *filename);
ZEND_API zend_result (*zend_post_startup_cb)(void) = NULL;
ZEND_API void (*zend_post_shutdown_cb)(void) = NULL;
ZEND_API void (*zend_accel_schedule_restart_hook)(int reason) = NULL;
ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce) = NULL;
ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data) = NULL;
ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce) = NULL;
ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce) = NULL;
ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref) = NULL;
ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish) = NULL;
ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj) = NULL;
ZEND_ATTRIBUTE_NONNULL ZEND_API zend_result (*zend_random_bytes)(void *bytes, size_t size, char *errstr, size_t errstr_size) = NULL;
ZEND_ATTRIBUTE_NONNULL ZEND_API void (*zend_random_bytes_insecure)(zend_random_bytes_insecure_state *state, void *bytes, size_t size) = NULL;

Expand Down Expand Up @@ -819,6 +826,8 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
#endif
executor_globals->saved_fpu_cw_ptr = NULL;
executor_globals->active = false;
executor_globals->static_cache_class_access_active = false;
executor_globals->tracked_mutation_hooks_active = false;
executor_globals->bailout = NULL;
executor_globals->error_handling = EH_NORMAL;
executor_globals->exception_class = NULL;
Expand Down
11 changes: 11 additions & 0 deletions Zend/zend.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,17 @@ extern ZEND_API void (*zend_post_shutdown_cb)(void);

extern ZEND_API void (*zend_accel_schedule_restart_hook)(int reason);

/* These hooks are used by OPcache Static Cache to restore, publish, and track
* selected VolatileStatic and PersistentStatic state across requests. They remain
* NULL when the static-cache subsystem is not active. */
extern ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce);
extern ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data);
extern ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce);
extern ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce);
extern ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref);
extern ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish);
extern ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj);

ZEND_API ZEND_COLD void zend_error(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn_unchecked(int type, const char *format, ...);
Expand Down
42 changes: 42 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -2908,6 +2908,11 @@ static zend_always_inline void zend_fetch_dimension_address(zval *result, zval *

if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) {
try_array:
if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) &&
EG(tracked_mutation_hooks_active))
) {
zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false);
}
SEPARATE_ARRAY(container);
fetch_from_array:
if (dim == NULL) {
Expand Down Expand Up @@ -3507,6 +3512,20 @@ static zend_never_inline bool zend_handle_fetch_obj_flags(
return 1;
}

static zend_always_inline void zend_track_property_array_indirect_mutation(zval *ptr, int type)
{
zval *tracked = ptr;

if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) &&
EG(tracked_mutation_hooks_active))
) {
ZVAL_DEREF(tracked);
if (Z_TYPE_P(tracked) == IS_ARRAY) {
zend_maybe_track_hash_mutation(Z_ARRVAL_P(tracked), false);
}
}
}

static zend_always_inline void zend_fetch_property_address(
zval *result,
const zval *container,
Expand Down Expand Up @@ -3591,6 +3610,7 @@ static zend_always_inline void zend_fetch_property_address(
zend_handle_fetch_obj_flags(result, ptr, NULL, prop_info, flags);
}
}
zend_track_property_array_indirect_mutation(ptr, type);
return;
}
} else if (UNEXPECTED(IS_HOOKED_PROPERTY_OFFSET(prop_offset))) {
Expand All @@ -3606,6 +3626,7 @@ static zend_always_inline void zend_fetch_property_address(
ptr = zend_hash_find_known_hash(zobj->properties, Z_STR_P(prop_ptr));
if (EXPECTED(ptr)) {
ZVAL_INDIRECT(result, ptr);
zend_track_property_array_indirect_mutation(ptr, type);
return;
}
}
Expand Down Expand Up @@ -3653,6 +3674,7 @@ static zend_always_inline void zend_fetch_property_address(
}
}
}
zend_track_property_array_indirect_mutation(ptr, type);

end:
if (prop_info_p) {
Expand Down Expand Up @@ -3828,6 +3850,12 @@ static zend_always_inline zval* zend_fetch_static_property_address(zend_property
result = CACHED_PTR(cache_slot + sizeof(void *));
property_info = CACHED_PTR(cache_slot + sizeof(void *) * 2);

if (UNEXPECTED(EG(static_cache_class_access_active) &&
zend_class_static_access_hook != NULL)
) {
zend_class_static_access_hook(property_info->ce);
}

if ((fetch_type == BP_VAR_R || fetch_type == BP_VAR_RW)
&& UNEXPECTED(Z_TYPE_P(result) == IS_UNDEF)
&& ZEND_TYPE_IS_SET(property_info->type)) {
Expand Down Expand Up @@ -4102,6 +4130,20 @@ ZEND_API zval* zend_assign_to_typed_ref(zval *variable_ptr, zval *orig_value, ui
return result;
}

zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_slow(zend_object *zobj)
{
if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL) {
zend_tracked_object_mutation_hook(zobj);
}
}

zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_with_value_slow(zend_object *zobj, zval *value)
{
if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL && value != &EG(error_zval)) {
zend_tracked_object_mutation_hook(zobj);
}
}

ZEND_API bool ZEND_FASTCALL zend_verify_prop_assignable_by_ref_ex(const zend_property_info *prop_info, zval *orig_val, bool strict, zend_verify_prop_assignable_by_ref_context context) {
zval *val = orig_val;
if (Z_ISREF_P(val) && ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(val))) {
Expand Down
Loading
Loading