From 0d542e6da3906ef80b0a9fe48d85ba83009be52d Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 8 Aug 2026 09:34:22 +0500 Subject: [PATCH 1/3] Fix GH-23115: stack overflow in compact() with deep arrays --- NEWS | 4 ++++ ext/standard/array.c | 7 +++++++ ext/standard/tests/array/gh23115.phpt | 27 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 ext/standard/tests/array/gh23115.phpt diff --git a/NEWS b/NEWS index ba832ef05888..b0681c75d840 100644 --- a/NEWS +++ b/NEWS @@ -52,6 +52,10 @@ PHP NEWS - SQLite: . Fix leak when trying to close db if blob stream is still open. (ndossche) +- Standard: + . Fixed bug GH-23115 (Stack overflow in compact() with deeply nested + arrays). (Lazizbek Ergashev) + - Streams: . Fixed bug GH-15836 (Use-after-free when a user stream filter accesses $this->stream during the close flush). (iliaal) diff --git a/ext/standard/array.c b/ext/standard/array.c index 4527d9a80df8..4b59dae2733f 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -2696,6 +2696,13 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu { zval *value_ptr, data; +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_call_stack_size_error(); + return; + } +#endif + ZVAL_DEREF(entry); if (Z_TYPE_P(entry) == IS_STRING) { if ((value_ptr = zend_hash_find_ind(eg_active_symbol_table, Z_STR_P(entry))) != NULL) { diff --git a/ext/standard/tests/array/gh23115.phpt b/ext/standard/tests/array/gh23115.phpt new file mode 100644 index 000000000000..0de1b871f24f --- /dev/null +++ b/ext/standard/tests/array/gh23115.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-23115 (Stack overflow in compact with deeply nested arrays) +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECTF-- +Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion? From 6edb35431db8a2930436494b3b839c676841381a Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 10 Aug 2026 15:12:04 +0500 Subject: [PATCH 2/3] compact(): stop iterating once php_compact_var() has thrown --- ext/standard/array.c | 19 ++++++++++++------- ext/standard/tests/array/gh23115.phpt | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 4b59dae2733f..77f63e6e60f9 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -2696,13 +2696,6 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu { zval *value_ptr, data; -#ifdef ZEND_CHECK_STACK_LIMIT - if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { - zend_call_stack_size_error(); - return; - } -#endif - ZVAL_DEREF(entry); if (Z_TYPE_P(entry) == IS_STRING) { if ((value_ptr = zend_hash_find_ind(eg_active_symbol_table, Z_STR_P(entry))) != NULL) { @@ -2719,6 +2712,12 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu php_error_docref_unchecked(NULL, E_WARNING, "Undefined variable $%S", Z_STR_P(entry)); } } else if (Z_TYPE_P(entry) == IS_ARRAY) { +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_call_stack_size_error(); + return; + } +#endif if (Z_REFCOUNTED_P(entry)) { if (Z_IS_RECURSIVE_P(entry)) { zend_throw_error(NULL, "Recursion detected"); @@ -2728,6 +2727,9 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu } ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entry), value_ptr) { php_compact_var(eg_active_symbol_table, return_value, value_ptr, pos); + if (UNEXPECTED(EG(exception))) { + break; + } } ZEND_HASH_FOREACH_END(); if (Z_REFCOUNTED_P(entry)) { Z_UNPROTECT_RECURSION_P(entry); @@ -2768,6 +2770,9 @@ PHP_FUNCTION(compact) for (i = 0; i < num_args; i++) { php_compact_var(symbol_table, return_value, &args[i], i + 1); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } } } /* }}} */ diff --git a/ext/standard/tests/array/gh23115.phpt b/ext/standard/tests/array/gh23115.phpt index 0de1b871f24f..7854583f9ec6 100644 --- a/ext/standard/tests/array/gh23115.phpt +++ b/ext/standard/tests/array/gh23115.phpt @@ -13,15 +13,29 @@ if (getenv('SKIP_ASAN')) { zend.max_allowed_stack_size=256K --FILE-- getMessage(), "\n"; + var_dump($e->getPrevious()); +} + +try { + compact($names, $names); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + var_dump($e->getPrevious()); } ?> --EXPECTF-- Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion? +NULL +Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion? +NULL From f9e124a3e503eed4d7d58acc3a77c64b6841d373 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 10 Aug 2026 15:26:44 +0500 Subject: [PATCH 3/3] compact(): return zend_result from php_compact_var() --- ext/standard/array.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 77f63e6e60f9..68843a661604 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -2692,7 +2692,7 @@ PHP_FUNCTION(extract) } /* }}} */ -static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_value, zval *entry, uint32_t pos) /* {{{ */ +static zend_result php_compact_var(HashTable *eg_active_symbol_table, zval *return_value, zval *entry, uint32_t pos) /* {{{ */ { zval *value_ptr, data; @@ -2710,34 +2710,43 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu } } else { php_error_docref_unchecked(NULL, E_WARNING, "Undefined variable $%S", Z_STR_P(entry)); + /* A user error handler may have thrown. */ + return EG(exception) ? FAILURE : SUCCESS; } } else if (Z_TYPE_P(entry) == IS_ARRAY) { + zend_result result = SUCCESS; + #ifdef ZEND_CHECK_STACK_LIMIT if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { zend_call_stack_size_error(); - return; + return FAILURE; } #endif if (Z_REFCOUNTED_P(entry)) { if (Z_IS_RECURSIVE_P(entry)) { zend_throw_error(NULL, "Recursion detected"); - return; + return FAILURE; } Z_PROTECT_RECURSION_P(entry); } ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entry), value_ptr) { - php_compact_var(eg_active_symbol_table, return_value, value_ptr, pos); - if (UNEXPECTED(EG(exception))) { + if (UNEXPECTED(php_compact_var(eg_active_symbol_table, return_value, value_ptr, pos) == FAILURE)) { + result = FAILURE; break; } } ZEND_HASH_FOREACH_END(); if (Z_REFCOUNTED_P(entry)) { Z_UNPROTECT_RECURSION_P(entry); } + + return result; } else { php_error_docref(NULL, E_WARNING, "Argument #%d must be string or array of strings, %s given", pos, zend_zval_value_name(entry)); - return; + /* A user error handler may have thrown. */ + return EG(exception) ? FAILURE : SUCCESS; } + + return SUCCESS; } /* }}} */ @@ -2769,8 +2778,7 @@ PHP_FUNCTION(compact) } for (i = 0; i < num_args; i++) { - php_compact_var(symbol_table, return_value, &args[i], i + 1); - if (UNEXPECTED(EG(exception))) { + if (UNEXPECTED(php_compact_var(symbol_table, return_value, &args[i], i + 1) == FAILURE)) { RETURN_THROWS(); } }