From bcd4be7d50b3632e6191b880bd9ab458cc615b08 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 30 Sep 2025 00:54:41 +0200 Subject: [PATCH 1/3] Fix double-free of EG(errors)/persistent_script->warnings on persist of already persisted file Both processes race to compile warning_replay.inc. Whichever is first will get to persist the script. The loser will use the script that is already persisted, and the script that was just compiled is freed. However, EG(errors) and persistent_script->warnings still refer to the same allocation, and EG(errors) becomes a dangling pointer. To solve this, we simply don't free warnings from free_persistent_script() anymore to maintain exclusive ownership for EG(errors). Furthermore, we need to adjust a call to zend_emit_recorded_errors() that would previously use EG(errors), even when persistent_script has been swapped out. Fixes GH-19984 Closes GH-19995 --- NEWS | 2 ++ ext/opcache/ZendAccelerator.c | 9 ++++++++- ext/opcache/tests/gh19984.phpt | 21 +++++++++++++++++++++ ext/opcache/zend_accelerator_util_funcs.c | 10 ---------- 4 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 ext/opcache/tests/gh19984.phpt diff --git a/NEWS b/NEWS index 740755ecd407..575fcdbbad97 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,8 @@ PHP NEWS - Opcache: . Fixed segfault in function JIT due to NAN to bool warning. (Girgias) + . Fixed bug GH-19984 (Double-free of EG(errors)/persistent_script->warnings on + persist of already persisted file). (ilutov, Arnaud) - SOAP: . Fixed bug GH-19773 (SIGSEGV due to uninitialized soap_globals->lang_en). diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index f0d337953459..0456017dae05 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -2209,7 +2209,14 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) SHM_PROTECT(); HANDLE_UNBLOCK_INTERRUPTIONS(); - zend_emit_recorded_errors(); + /* We may have switched to an existing persistent script that was persisted in + * the meantime. Make sure to use its warnings if available. */ + if (ZCG(accel_directives).record_warnings) { + EG(record_errors) = false; + zend_emit_recorded_errors_ex(persistent_script->num_warnings, persistent_script->warnings); + } else { + zend_emit_recorded_errors(); + } zend_free_recorded_errors(); } else { diff --git a/ext/opcache/tests/gh19984.phpt b/ext/opcache/tests/gh19984.phpt new file mode 100644 index 000000000000..4584fa6494c1 --- /dev/null +++ b/ext/opcache/tests/gh19984.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-19984: Double-free of EG(errors)/persistent_script->warnings on persist of already persisted file +--EXTENSIONS-- +opcache +pcntl +--INI-- +opcache.enable_cli=1 +opcache.record_warnings=1 +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Warning: Unsupported declare 'unknown' in %s on line %d + +Warning: Unsupported declare 'unknown' in %s on line %d diff --git a/ext/opcache/zend_accelerator_util_funcs.c b/ext/opcache/zend_accelerator_util_funcs.c index 21f056901fd1..99523ca72279 100644 --- a/ext/opcache/zend_accelerator_util_funcs.c +++ b/ext/opcache/zend_accelerator_util_funcs.c @@ -65,16 +65,6 @@ void free_persistent_script(zend_persistent_script *persistent_script, int destr zend_string_release_ex(persistent_script->script.filename, 0); } - if (persistent_script->warnings) { - for (uint32_t i = 0; i < persistent_script->num_warnings; i++) { - zend_error_info *info = persistent_script->warnings[i]; - zend_string_release(info->filename); - zend_string_release(info->message); - efree(info); - } - efree(persistent_script->warnings); - } - zend_accel_free_delayed_early_binding_list(persistent_script); efree(persistent_script); From 292e0c293717ff305527e6b4eff69bedda33c8f7 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 30 Sep 2025 22:54:59 +0200 Subject: [PATCH 2/3] Add ce_flags2 & fn_flags2 (GH-19991) --- Zend/Optimizer/zend_optimizer.c | 2 ++ Zend/zend.h | 1 + Zend/zend_compile.c | 1 + Zend/zend_compile.h | 13 +++++++++++++ Zend/zend_execute.c | 1 + Zend/zend_object_handlers.c | 2 ++ Zend/zend_opcode.c | 1 + ext/ffi/ffi.c | 2 ++ ext/opcache/ZendAccelerator.c | 2 ++ ext/zend_test/test.c | 2 ++ 10 files changed, 27 insertions(+) diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index a206238b552b..5d18fc601585 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1727,11 +1727,13 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l ZEND_ASSERT(orig_op_array != NULL); if (orig_op_array != op_array) { uint32_t fn_flags = op_array->fn_flags; + uint32_t fn_flags2 = op_array->fn_flags2; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; *op_array = *orig_op_array; op_array->fn_flags = fn_flags; + op_array->fn_flags2 = fn_flags2; op_array->prototype = prototype; op_array->static_variables = ht; } diff --git a/Zend/zend.h b/Zend/zend.h index 53d5602a2c2e..163b48015d9b 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -154,6 +154,7 @@ struct _zend_class_entry { }; int refcount; uint32_t ce_flags; + uint32_t ce_flags2; int default_properties_count; int default_static_members_count; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ff790137a90f..57c012ac71a6 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2048,6 +2048,7 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->refcount = 1; ce->ce_flags = ZEND_ACC_CONSTANTS_UPDATED; + ce->ce_flags2 = 0; if (CG(compiler_options) & ZEND_COMPILE_GUARDS) { ce->ce_flags |= ZEND_ACC_USE_GUARDS; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index ab90b2dcaa83..4a10ad99e242 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -341,6 +341,11 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ +/* Class Flags 2 (ce_flags2) (unused: 0-31) | | | */ +/* ========================= | | | */ +/* | | | */ +/* #define ZEND_ACC2_EXAMPLE (1 << 0) X | | | */ +/* | | | */ /* Function Flags (unused: 30) | | | */ /* ============== | | | */ /* | | | */ @@ -407,6 +412,11 @@ typedef struct _zend_oparray_context { /* | | | */ /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ +/* | | | */ +/* Function Flags 2 (fn_flags2) (unused: 0-31) | | | */ +/* ============================ | | | */ +/* | | | */ +/* #define ZEND_ACC2_EXAMPLE (1 << 0) | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) @@ -527,6 +537,7 @@ struct _zend_op_array { ZEND_MAP_PTR_DEF(void **, run_time_cache); zend_string *doc_comment; uint32_t T; /* number of temporary variables */ + uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ /* END of common elements */ @@ -586,6 +597,7 @@ typedef struct _zend_internal_function { ZEND_MAP_PTR_DEF(void **, run_time_cache); zend_string *doc_comment; uint32_t T; /* number of temporary variables */ + uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ /* END of common elements */ @@ -615,6 +627,7 @@ union _zend_function { ZEND_MAP_PTR_DEF(void **, run_time_cache); zend_string *doc_comment; uint32_t T; /* number of temporary variables */ + uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ } common; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 331043d3fef9..c8863a4b27ad 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -153,6 +153,7 @@ ZEND_API const zend_internal_function zend_pass_function = { NULL, /* run_time_cache */ NULL, /* doc_comment */ 0, /* T */ + 0, /* fn_flags2 */ NULL, /* prop_info */ ZEND_FN(pass), /* handler */ NULL, /* module */ diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 361ddde6ebab..5d1ec65ccda4 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1696,6 +1696,7 @@ ZEND_API zend_function *zend_get_call_trampoline_func(const zend_class_entry *ce | ZEND_ACC_PUBLIC | ZEND_ACC_VARIADIC | (fbc->common.fn_flags & (ZEND_ACC_RETURN_REFERENCE|ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD)); + func->fn_flags2 = 0; /* Attributes outlive the trampoline because they are created by the compiler. */ func->attributes = fbc->common.attributes; if (is_static) { @@ -1797,6 +1798,7 @@ ZEND_API zend_function *zend_get_property_hook_trampoline( func->common.arg_flags[1] = 0; func->common.arg_flags[2] = 0; func->common.fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE; + func->common.fn_flags2 = 0; func->common.function_name = zend_string_concat3( "$", 1, ZSTR_VAL(prop_name), ZSTR_LEN(prop_name), kind == ZEND_PROPERTY_HOOK_GET ? "::get" : "::set", 5); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index bf02d6a4164b..1962c7b5a56d 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -84,6 +84,7 @@ void init_op_array(zend_op_array *op_array, uint8_t type, int initial_ops_size) op_array->last_try_catch = 0; op_array->fn_flags = 0; + op_array->fn_flags2 = 0; op_array->last_literal = 0; op_array->literals = NULL; diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c index 8e9f4290e1b2..86b8d29209f4 100644 --- a/ext/ffi/ffi.c +++ b/ext/ffi/ffi.c @@ -2190,6 +2190,7 @@ static zend_result zend_ffi_cdata_get_closure(zend_object *obj, zend_class_entry func->common.arg_flags[1] = 0; func->common.arg_flags[2] = 0; func->common.fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE; + func->common.fn_flags2 = 0; func->common.function_name = ZSTR_KNOWN(ZEND_STR_MAGIC_INVOKE); /* set to 0 to avoid arg_info[] allocation, because all values are passed by value anyway */ func->common.num_args = 0; @@ -2969,6 +2970,7 @@ static zend_function *zend_ffi_get_func(zend_object **obj, zend_string *name, co func->common.arg_flags[1] = 0; func->common.arg_flags[2] = 0; func->common.fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE; + func->common.fn_flags2 = 0; func->common.function_name = zend_string_copy(name); /* set to 0 to avoid arg_info[] allocation, because all values are passed by value anyway */ func->common.num_args = 0; diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 0456017dae05..9ef74973a4df 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4367,12 +4367,14 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) zend_string *function_name = op_array->function_name; zend_class_entry *scope = op_array->scope; uint32_t fn_flags = op_array->fn_flags; + uint32_t fn_flags2 = op_array->fn_flags2; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; *op_array = *orig_op_array; op_array->function_name = function_name; op_array->scope = scope; op_array->fn_flags = fn_flags; + op_array->fn_flags2 = fn_flags2; op_array->prototype = prototype; op_array->static_variables = ht; } diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 50c0a57ae8ef..4e06b2106ce5 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1053,6 +1053,7 @@ static zend_function *zend_test_class_method_get(zend_object **object, zend_stri fptr->num_args = 0; fptr->scope = (*object)->ce; fptr->fn_flags = ZEND_ACC_CALL_VIA_HANDLER; + fptr->fn_flags2 = 0; fptr->function_name = zend_string_copy(name); fptr->handler = ZEND_FN(zend_test_func); fptr->doc_comment = NULL; @@ -1077,6 +1078,7 @@ static zend_function *zend_test_class_static_method_get(zend_class_entry *ce, ze fptr->num_args = 0; fptr->scope = ce; fptr->fn_flags = ZEND_ACC_CALL_VIA_HANDLER|ZEND_ACC_STATIC; + fptr->fn_flags2 = 0; fptr->function_name = zend_string_copy(name); fptr->handler = ZEND_FN(zend_test_func); fptr->doc_comment = NULL; From a3f0861f2e5c287b200bd5692e00569b10bb9173 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 30 Sep 2025 22:58:30 +0200 Subject: [PATCH 3/3] [skip ci] Mention new ce_flags2 and fn_flags2 fields in UPGRADING.INTERNALS --- UPGRADING.INTERNALS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 0c9aeb0d32f2..c2ec44e1e0c2 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -18,6 +18,8 @@ PHP 8.6 INTERNALS UPGRADE NOTES zend_string_starts_with_literal_ci() now support strings containing NUL bytes. Passing non-literal char* is no longer supported. . The misnamed ZVAL_IS_NULL() has been removed. Use Z_ISNULL() instead. + . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were + added, given the primary flags were running out of bits. ======================== 2. Build system changes