Commit 0ada938f88d for php.net

commit 0ada938f88d01c7b708981702e2b6dd38d6f76b8
Author: David CARLIER <devnexen@gmail.com>
Date:   Mon Mar 30 18:36:01 2026 +0100

     Zend: Preallocate error buffer with capacity tracking (#20565)

    Replace separate num_errors/errors fields with a single struct containing
    size, capacity, and a flexible array. The buffer grows by 50% when needed
    instead of reallocating on every recorded error.

diff --git a/Zend/zend.c b/Zend/zend.c
index af0013220f2..2a5988273bc 100644
--- a/Zend/zend.c
+++ b/Zend/zend.c
@@ -833,8 +833,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
 #endif
 	executor_globals->flags = EG_FLAGS_INITIAL;
 	executor_globals->record_errors = false;
-	executor_globals->num_errors = 0;
-	executor_globals->errors = NULL;
+	memset(&executor_globals->errors, 0, sizeof(executor_globals->errors));
 	executor_globals->filename_override = NULL;
 	executor_globals->lineno_override = -1;
 #ifdef ZEND_CHECK_STACK_LIMIT
@@ -1446,8 +1445,7 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 	zend_stack delayed_oplines_stack;
 	int type = orig_type & E_ALL;
 	bool orig_record_errors;
-	uint32_t orig_num_errors;
-	zend_error_info **orig_errors;
+	zend_err_buf orig_errors_buf;
 	zend_result res;

 	/* If we're executing a function during SCCP, count any warnings that may be emitted,
@@ -1459,11 +1457,9 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 	}

 	/* Emit any delayed error before handling fatal error */
-	if ((type & E_FATAL_ERRORS) && !(type & E_DONT_BAIL) && EG(num_errors)) {
-		uint32_t num_errors = EG(num_errors);
-		zend_error_info **errors = EG(errors);
-		EG(num_errors) = 0;
-		EG(errors) = NULL;
+	if ((type & E_FATAL_ERRORS) && !(type & E_DONT_BAIL) && EG(errors).size) {
+		zend_err_buf errors_buf = EG(errors);
+		EG(errors).size = 0;

 		bool orig_record_errors = EG(record_errors);
 		EG(record_errors) = false;
@@ -1473,12 +1469,11 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 		int orig_user_error_handler_error_reporting = EG(user_error_handler_error_reporting);
 		EG(user_error_handler_error_reporting) = 0;

-		zend_emit_recorded_errors_ex(num_errors, errors);
+		zend_emit_recorded_errors_ex(errors_buf.size, errors_buf.errors);

 		EG(user_error_handler_error_reporting) = orig_user_error_handler_error_reporting;
 		EG(record_errors) = orig_record_errors;
-		EG(num_errors) = num_errors;
-		EG(errors) = errors;
+		EG(errors) = errors_buf;
 	}

 	if (EG(record_errors)) {
@@ -1487,12 +1482,13 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 		info->lineno = error_lineno;
 		info->filename = zend_string_copy(error_filename);
 		info->message = zend_string_copy(message);
-
-		/* This is very inefficient for a large number of errors.
-		 * Use pow2 realloc if it becomes a problem. */
-		EG(num_errors)++;
-		EG(errors) = erealloc(EG(errors), sizeof(zend_error_info*) * EG(num_errors));
-		EG(errors)[EG(num_errors)-1] = info;
+		EG(errors).size++;
+		if (EG(errors).size > EG(errors).capacity) {
+			uint32_t capacity = EG(errors).capacity ? EG(errors).capacity + (EG(errors).capacity >> 1) : 2;
+			EG(errors).errors = erealloc(EG(errors).errors, sizeof(zend_error_info *) * capacity);
+			EG(errors).capacity = capacity;
+		}
+		EG(errors).errors[EG(errors).size - 1] = info;

 		/* Do not process non-fatal recorded error */
 		if (!(type & E_FATAL_ERRORS) || (type & E_DONT_BAIL)) {
@@ -1575,17 +1571,15 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
 			}

 			orig_record_errors = EG(record_errors);
-			orig_num_errors = EG(num_errors);
-			orig_errors = EG(errors);
 			EG(record_errors) = false;
-			EG(num_errors) = 0;
-			EG(errors) = NULL;
+
+			orig_errors_buf = EG(errors);
+			memset(&EG(errors), 0, sizeof(EG(errors)));

 			res = call_user_function(CG(function_table), NULL, &orig_user_error_handler, &retval, 4, params);

 			EG(record_errors) = orig_record_errors;
-			EG(num_errors) = orig_num_errors;
-			EG(errors) = orig_errors;
+			EG(errors) = orig_errors_buf;

 			if (res == SUCCESS) {
 				if (Z_TYPE(retval) != IS_UNDEF) {
@@ -1780,8 +1774,7 @@ ZEND_API void zend_begin_record_errors(void)
 {
 	ZEND_ASSERT(!EG(record_errors) && "Error recording already enabled");
 	EG(record_errors) = true;
-	EG(num_errors) = 0;
-	EG(errors) = NULL;
+	EG(errors).size = 0;
 }

 ZEND_API void zend_emit_recorded_errors_ex(uint32_t num_errors, zend_error_info **errors)
@@ -1795,24 +1788,23 @@ ZEND_API void zend_emit_recorded_errors_ex(uint32_t num_errors, zend_error_info
 ZEND_API void zend_emit_recorded_errors(void)
 {
 	EG(record_errors) = false;
-	zend_emit_recorded_errors_ex(EG(num_errors), EG(errors));
+	zend_emit_recorded_errors_ex(EG(errors).size, EG(errors).errors);
 }

 ZEND_API void zend_free_recorded_errors(void)
 {
-	if (!EG(num_errors)) {
+	if (!EG(errors).size) {
 		return;
 	}

-	for (uint32_t i = 0; i < EG(num_errors); i++) {
-		zend_error_info *info = EG(errors)[i];
+	for (uint32_t i = 0; i < EG(errors).size; i++) {
+		zend_error_info *info = EG(errors).errors[i];
 		zend_string_release(info->filename);
 		zend_string_release(info->message);
-		efree(info);
+		efree_size(info, sizeof(zend_error_info));
 	}
-	efree(EG(errors));
-	EG(errors) = NULL;
-	EG(num_errors) = 0;
+	efree(EG(errors).errors);
+	memset(&EG(errors), 0, sizeof(EG(errors)));
 }

 ZEND_API ZEND_COLD void zend_throw_error(zend_class_entry *exception_ce, const char *format, ...) /* {{{ */
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index dbd2a9039cf..30ed4f5914c 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -192,8 +192,7 @@ void init_executor(void) /* {{{ */
 	EG(get_gc_buffer).start = EG(get_gc_buffer).end = EG(get_gc_buffer).cur = NULL;

 	EG(record_errors) = false;
-	EG(num_errors) = 0;
-	EG(errors) = NULL;
+	memset(&EG(errors), 0, sizeof(EG(errors)));

 	EG(filename_override) = NULL;
 	EG(lineno_override) = -1;
diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h
index 31f54cd8284..db202dda66c 100644
--- a/Zend/zend_globals.h
+++ b/Zend/zend_globals.h
@@ -74,6 +74,7 @@ typedef struct _zend_vm_stack *zend_vm_stack;
 typedef struct _zend_ini_entry zend_ini_entry;
 typedef struct _zend_fiber_context zend_fiber_context;
 typedef struct _zend_fiber zend_fiber;
+typedef struct _zend_error_info zend_error_info;

 typedef enum {
 	ZEND_MEMOIZE_NONE,
@@ -81,6 +82,12 @@ typedef enum {
 	ZEND_MEMOIZE_FETCH,
 } zend_memoize_mode;

+typedef struct zend_err_buf {
+	uint32_t size;
+	uint32_t capacity;
+	zend_error_info **errors;
+} zend_err_buf;
+
 struct _zend_compiler_globals {
 	zend_stack loop_var_stack;

@@ -298,8 +305,7 @@ struct _zend_executor_globals {
 	 * and their processing is delayed until zend_emit_recorded_errors()
 	 * is called or a fatal diagnostic is emitted. */
 	bool record_errors;
-	uint32_t num_errors;
-	zend_error_info **errors;
+	zend_err_buf errors;

 	/* Override filename or line number of thrown errors and exceptions */
 	zend_string *filename_override;
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 3d6923d1215..7aa0e67c24a 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -1968,8 +1968,8 @@ static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int

 	if (persistent_script) {
 		if (ZCG(accel_directives).record_warnings) {
-			persistent_script->num_warnings = EG(num_errors);
-			persistent_script->warnings = EG(errors);
+			persistent_script->num_warnings = EG(errors).size;
+			persistent_script->warnings = EG(errors).errors;
 		}

 		from_memory = false;
@@ -2193,8 +2193,8 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
 		from_shared_memory = false;
 		if (persistent_script) {
 			if (ZCG(accel_directives).record_warnings) {
-				persistent_script->num_warnings = EG(num_errors);
-				persistent_script->warnings = EG(errors);
+				persistent_script->num_warnings = EG(errors).size;
+				persistent_script->warnings = EG(errors).errors;
 			}

 			/* See GH-17246: we disable GC so that user code cannot be executed during the optimizer run. */
@@ -2421,7 +2421,7 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce,
 	}
 	ZCG(current_persistent_script) = &dummy;
 	zend_persist_class_entry_calc(ce);
-	zend_persist_warnings_calc(EG(num_errors), EG(errors));
+	zend_persist_warnings_calc(EG(errors).size, EG(errors).errors);
 	size = dummy.size;

 	zend_shared_alloc_clear_xlat_table();
@@ -2491,8 +2491,8 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce,
 	JIT_G(on) = jit_on_old;
 #endif

-	entry->num_warnings = EG(num_errors);
-	entry->warnings = zend_persist_warnings(EG(num_errors), EG(errors));
+	entry->num_warnings = EG(errors).size;
+	entry->warnings = zend_persist_warnings(EG(errors).size, EG(errors).errors);
 	entry->next = proto->inheritance_cache;
 	proto->inheritance_cache = entry;

@@ -4123,9 +4123,9 @@ static void preload_link(void)
 				/* Remember the last error. */
 				zend_error_cb = orig_error_cb;
 				EG(record_errors) = false;
-				ZEND_ASSERT(EG(num_errors) > 0);
-				zend_hash_update_ptr(&errors, key, EG(errors)[EG(num_errors)-1]);
-				EG(num_errors)--;
+				ZEND_ASSERT(EG(errors).size > 0);
+				zend_hash_update_ptr(&errors, key, EG(errors).errors[EG(errors).size-1]);
+				EG(errors).size--;
 			} zend_end_try();
 			CG(in_compilation) = false;
 			CG(compiled_filename) = NULL;