From beee995c90598189e046e2a4c75b0bd5caa4b744 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 25 Sep 2026 02:16:31 -0400 Subject: [PATCH 01/23] ext/pdo: Keep statement class when ATTR_STATEMENT_CLASS is rejected pdo_dbh_attribute_set() installed the new PDO::ATTR_STATEMENT_CLASS class and released the old constructor arguments before checking that constructor_args is an array, so a rejected setAttribute() left the new class in place for the next prepare() or query(). Validate constructor_args first, and report its type rather than the whole value's in the TypeError, here and in prepare(). Closes GH-23968 --- NEWS | 4 ++++ ext/pdo/pdo_dbh.c | 15 ++++++++------- ext/pdo/tests/pdo_030.phpt | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index 2e5b71aa8fc0..85b41dfb7cae 100644 --- a/NEWS +++ b/NEWS @@ -102,6 +102,10 @@ PHP NEWS that is not in the result set. (Ilia Alshanetsky) . Fixed bug GH-23962 (Destroying a persistent PDO instance rolls back a transaction still in use by another instance). (Lazizbek Ergashev) + . Fixed PDO::setAttribute() installing a PDO::ATTR_STATEMENT_CLASS class + whose constructor arguments it rejects. (Ilia Alshanetsky) + . Fixed PDO::ATTR_STATEMENT_CLASS constructor_args type errors reporting + "array given" regardless of the value passed. (Ilia Alshanetsky) - PDO_Firebird: . Fixed bug GH-23758 (PDO_Firebird returns null for non-null empty BLOBs). diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 1dc43c2b9728..d3fcf6101249 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -631,7 +631,7 @@ PHP_METHOD(PDO, prepare) if ((item = zend_hash_index_find(Z_ARRVAL_P(value), 1)) != NULL) { if (Z_TYPE_P(item) != IS_ARRAY) { zend_type_error("PDO::ATTR_STATEMENT_CLASS constructor_args must be of type ?array, %s given", - zend_zval_value_name(value)); + zend_zval_value_name(item)); RETURN_THROWS(); } ZVAL_COPY_VALUE(&ctor_args, item); @@ -925,17 +925,18 @@ static bool pdo_dbh_attribute_set(pdo_dbh_t *dbh, zend_long attr, zval *value) / zend_type_error("User-supplied statement class cannot have a public constructor"); return false; } + item = zend_hash_index_find(Z_ARRVAL_P(value), 1); + if (item != NULL && Z_TYPE_P(item) != IS_ARRAY) { + zend_type_error("PDO::ATTR_STATEMENT_CLASS constructor_args must be of type ?array, %s given", + zend_zval_value_name(item)); + return false; + } dbh->def_stmt_ce = pce; if (!Z_ISUNDEF(dbh->def_stmt_ctor_args)) { zval_ptr_dtor(&dbh->def_stmt_ctor_args); ZVAL_UNDEF(&dbh->def_stmt_ctor_args); } - if ((item = zend_hash_index_find(Z_ARRVAL_P(value), 1)) != NULL) { - if (Z_TYPE_P(item) != IS_ARRAY) { - zend_type_error("PDO::ATTR_STATEMENT_CLASS constructor_args must be of type ?array, %s given", - zend_zval_value_name(value)); - return false; - } + if (item != NULL) { ZVAL_COPY(&dbh->def_stmt_ctor_args, item); } return true; diff --git a/ext/pdo/tests/pdo_030.phpt b/ext/pdo/tests/pdo_030.phpt index 88308c18ca83..097551211a02 100644 --- a/ext/pdo/tests/pdo_030.phpt +++ b/ext/pdo/tests/pdo_030.phpt @@ -75,6 +75,17 @@ echo "===QUERY===\n"; var_dump($db->getAttribute(PDO::ATTR_STATEMENT_CLASS)); $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatementx', array($db))); var_dump($db->getAttribute(PDO::ATTR_STATEMENT_CLASS)); +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, 'invalid-args']); +} catch (TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($db->getAttribute(PDO::ATTR_STATEMENT_CLASS)); +try { + $db->prepare('SELECT * FROM test030', [PDO::ATTR_STATEMENT_CLASS => [PDOStatement::class, 'invalid-args']]); +} catch (TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} $stmt = $db->query('SELECT * FROM test030'); var_dump(get_class($stmt)); var_dump(get_class($stmt->dbh)); @@ -112,6 +123,18 @@ array(2) { } } } +TypeError: PDO::ATTR_STATEMENT_CLASS constructor_args must be of type ?array, string given +array(2) { + [0]=> + string(13) "PDOStatementX" + [1]=> + array(1) { + [0]=> + object(PDODatabase)#%d (0) { + } + } +} +TypeError: PDO::ATTR_STATEMENT_CLASS constructor_args must be of type ?array, string given PDODatabase::query() PDOStatementX::__construct() string(13) "PDOStatementX" From 3bb50c13a7a1819b2a1d6d160434a93f2c4f2faa Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 26 Sep 2026 19:22:20 +0100 Subject: [PATCH 02/23] fibers: fix GH-23921 (Fibers start with error_reporting = 0 when the error_reporting INI directive is not set) The way zend_fiber_execute() queries the error_reporting level is rather rather strange. It cannot rely on the value of EG(error_reporting) as the @ silence operator may change it and this it would leak into the body of a fiber. However, we can simply query the underlying value of the INI setting to get the correct error_reporting value as this is not modified by the @ operator. Fixes an additional bug where the error_reporting value within a fiber was always E_ALL if the @ operator was used. Closes GH-23930 --- NEWS | 2 ++ Zend/tests/fibers/gh23921.phpt | 17 +++++++++++++++++ ...-fiber-should-no-widen-error-reporting.phpt | 18 ++++++++++++++++++ Zend/zend_fibers.c | 7 ++----- 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/fibers/gh23921.phpt create mode 100644 Zend/tests/fibers/silence-operator-outside-fiber-should-no-widen-error-reporting.phpt diff --git a/NEWS b/NEWS index 73b488bd4ac6..9e4e2f40c45c 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ PHP NEWS state. (Ilia Alshanetsky) . Fixed OSS-Fuzz #552682112 (assertion failure wrt zp_arg_must_be_sent_by_ref()). (ndossche) + . Fixed GH-23921 (Fibers start with error_reporting = 0 when the + error_reporting INI directive is not set). (Girgias) - FFI: . Fixed crashes with FFI callbacks created from __call() trampolines diff --git a/Zend/tests/fibers/gh23921.phpt b/Zend/tests/fibers/gh23921.phpt new file mode 100644 index 000000000000..62c2a458fa62 --- /dev/null +++ b/Zend/tests/fibers/gh23921.phpt @@ -0,0 +1,17 @@ +--TEST-- +GH-23921 (Fibers start with error_reporting = 0 when the error_reporting INI directive is not set) +--FILE-- +start(); +?> +--EXPECTF-- +30719 +30719 + +Warning: Undefined variable $undefined in %s on line 5 +after diff --git a/Zend/tests/fibers/silence-operator-outside-fiber-should-no-widen-error-reporting.phpt b/Zend/tests/fibers/silence-operator-outside-fiber-should-no-widen-error-reporting.phpt new file mode 100644 index 000000000000..9cb57c746936 --- /dev/null +++ b/Zend/tests/fibers/silence-operator-outside-fiber-should-no-widen-error-reporting.phpt @@ -0,0 +1,18 @@ +--TEST-- +Silence operator must not change the error_reporting value within the fiber +--FILE-- +start(); + +?> +--EXPECT-- +int(30719) +int(2) +int(2) diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index c91436050856..f56d89a4177c 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -570,12 +570,9 @@ static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer) zend_fiber *fiber = EG(active_fiber); - /* Determine the current error_reporting ini setting. */ + /* We cannot rely on EG(error_reporting) as a silence operator @ may modify the executor global error_reporting + * value without changing the underlying INI value */ zend_long error_reporting = zend_ini_long_literal("error_reporting"); - /* If error_reporting is 0 and not explicitly set to 0, zend_ini_str returns a null pointer. */ - if (!error_reporting && !zend_ini_str_literal("error_reporting")) { - error_reporting = E_ALL; - } EG(vm_stack) = NULL; From 37d0ef5a66ce66dcf40dd568fda796fd41a97d9d Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 29 Sep 2026 16:30:30 +0200 Subject: [PATCH 03/23] Zend: Remove zend_atomic.[ch] abstraction (#23927) --- UPGRADING.INTERNALS | 6 +- Zend/zend_atomic.c | 75 ------ Zend/zend_atomic.h | 407 ------------------------------- Zend/zend_execute.c | 10 +- Zend/zend_execute_API.c | 28 +-- Zend/zend_globals.h | 5 +- Zend/zend_portability.h | 17 ++ Zend/zend_vm_def.h | 4 +- Zend/zend_vm_execute.h | 8 +- configure.ac | 1 - ext/opcache/ZendAccelerator.c | 4 +- ext/opcache/jit/zend_jit_trace.c | 2 +- ext/pcntl/pcntl.c | 4 +- ext/random/csprng.c | 9 +- ext/zend_test/object_handlers.c | 2 +- ext/zend_test/observer.c | 2 +- sapi/phpdbg/phpdbg_prompt.c | 2 +- win32/build/config.w32 | 2 +- win32/signal.c | 4 +- 19 files changed, 63 insertions(+), 529 deletions(-) delete mode 100644 Zend/zend_atomic.c delete mode 100644 Zend/zend_atomic.h diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 0f79ee786aab..82f273974cd5 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -17,6 +17,8 @@ PHP 8.7 INTERNALS UPGRADE NOTES - Removed zend_execute_scripts(). Manually call zend_execute_script() in a loop instead. - Removed ZEND_STATIC_ASSERT(). Use C11 static_assert() directly instead. +- Removed zend_atomic.h and the zend_atomic_* types and functions. + Use the standard C11 atomic_* types and functions instead. - The sapi_terminate_process() function was removed. - The sapi_force_http_10() function was removed. - The sapi_get_fd() function was removed. @@ -33,8 +35,8 @@ PHP 8.7 INTERNALS UPGRADE NOTES ======================== - Windows build system changes: - . C sources are now compiled as C17. MSVC builds require Visual Studio - 2026 or later and enable /experimental:c11atomics. + . C sources are now compiled as C17 and require C11 atomics. MSVC builds require + Visual Studio 2026 or later and enable /experimental:c11atomics. . C++-only extension compiler flags can be added to CXXFLAGS_. - Unix build system changes: diff --git a/Zend/zend_atomic.c b/Zend/zend_atomic.c deleted file mode 100644 index 98a39c583437..000000000000 --- a/Zend/zend_atomic.c +++ /dev/null @@ -1,75 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Levi Morrison | - +----------------------------------------------------------------------+ - */ - -#include "zend_atomic.h" - -/* This file contains the non-inline copy of atomic functions. This is useful - * for extensions written in languages such as Rust. C and C++ compilers are - * probably going to inline these functions, but in the case they don't, this - * is also where the code will go. - */ - -/* Defined for FFI users; everyone else use ZEND_ATOMIC_*_INIT. - * This is NOT ATOMIC as it is meant for initialization. - */ -ZEND_API void zend_atomic_bool_init(zend_atomic_bool *obj, bool desired) { - ZEND_ATOMIC_BOOL_INIT(obj, desired); -} - -ZEND_API void zend_atomic_int_init(zend_atomic_int *obj, int desired) { - ZEND_ATOMIC_INT_INIT(obj, desired); -} - -ZEND_API bool zend_atomic_bool_exchange(zend_atomic_bool *obj, bool desired) { - return zend_atomic_bool_exchange_ex(obj, desired); -} - -ZEND_API int zend_atomic_int_exchange(zend_atomic_int *obj, int desired) { - return zend_atomic_int_exchange_ex(obj, desired); -} - -ZEND_API bool zend_atomic_bool_compare_exchange(zend_atomic_bool *obj, bool *expected, bool desired) -{ - return zend_atomic_bool_compare_exchange_ex(obj, expected, desired); -} - -ZEND_API bool zend_atomic_int_compare_exchange(zend_atomic_int *obj, int *expected, int desired) -{ - return zend_atomic_int_compare_exchange_ex(obj, expected, desired); -} - -ZEND_API void zend_atomic_bool_store(zend_atomic_bool *obj, bool desired) { - zend_atomic_bool_store_ex(obj, desired); -} - -ZEND_API void zend_atomic_int_store(zend_atomic_int *obj, int desired) { - zend_atomic_int_store_ex(obj, desired); -} - -#if (defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS)) && !defined(HAVE_C11_ATOMICS) -/* On these platforms it is non-const due to underlying APIs. */ -ZEND_API bool zend_atomic_bool_load(zend_atomic_bool *obj) { - return zend_atomic_bool_load_ex(obj); -} -ZEND_API int zend_atomic_int_load(zend_atomic_int *obj) { - return zend_atomic_int_load_ex(obj); -} -#else -ZEND_API bool zend_atomic_bool_load(const zend_atomic_bool *obj) { - return zend_atomic_bool_load_ex(obj); -} -ZEND_API int zend_atomic_int_load(const zend_atomic_int *obj) { - return zend_atomic_int_load_ex(obj); -} -#endif diff --git a/Zend/zend_atomic.h b/Zend/zend_atomic.h deleted file mode 100644 index 5dee78c0570a..000000000000 --- a/Zend/zend_atomic.h +++ /dev/null @@ -1,407 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Levi Morrison | - +----------------------------------------------------------------------+ - */ - -#ifndef ZEND_ATOMIC_H -#define ZEND_ATOMIC_H - -#include "zend_portability.h" - -#include - -#define ZEND_GCC_PREREQ(x, y) \ - ((__GNUC__ == (x) && __GNUC_MINOR__ >= (y)) || (__GNUC__ > (x))) - -/* Builtins are used to avoid library linkage */ -#if __has_feature(c_atomic) && defined(__clang__) -#define HAVE_C11_ATOMICS 1 -#elif ZEND_GCC_PREREQ(4, 7) -#define HAVE_GNUC_ATOMICS 1 -#elif defined(__GNUC__) -#define HAVE_SYNC_ATOMICS 1 -#elif !defined(ZEND_WIN32) -#define HAVE_NO_ATOMICS 1 -#endif - -#undef ZEND_GCC_PREREQ - -/* Treat zend_atomic_* types as opaque. They have definitions only for size - * and alignment purposes. - */ - -#if (defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS)) && !defined(HAVE_C11_ATOMICS) -typedef struct zend_atomic_bool_s { - volatile char value; -} zend_atomic_bool; -typedef struct zend_atomic_int_s { -# ifdef ZEND_WIN32 - volatile long value; -# else - volatile int value; -# endif -} zend_atomic_int; -#elif defined(HAVE_C11_ATOMICS) -typedef struct zend_atomic_bool_s { - _Atomic(bool) value; -} zend_atomic_bool; -typedef struct zend_atomic_int_s { - _Atomic(int) value; -} zend_atomic_int; -#else -typedef struct zend_atomic_bool_s { - volatile bool value; -} zend_atomic_bool; -typedef struct zend_atomic_int_s { - volatile int value; -} zend_atomic_int; -#endif - -BEGIN_EXTERN_C() - -#if defined(ZEND_WIN32) && !defined(HAVE_C11_ATOMICS) - -#ifndef InterlockedExchange8 -#define InterlockedExchange8 _InterlockedExchange8 -#endif -#ifndef InterlockedOr8 -#define InterlockedOr8 _InterlockedOr8 -#endif -#ifndef InterlockedCompareExchange8 -#define InterlockedCompareExchange8 _InterlockedCompareExchange8 -#endif -#ifndef InterlockedExchange -#define InterlockedExchange _InterlockedExchange -#endif -#ifndef InterlockedOr -#define InterlockedOr _InterlockedOr -#endif -#ifndef InterlockedCompareExchange -#define InterlockedCompareExchange _InterlockedCompareExchange -#endif - -#define ZEND_ATOMIC_BOOL_INIT(obj, desired) ((obj)->value = (desired)) -#define ZEND_ATOMIC_INT_INIT(obj, desired) ((obj)->value = (desired)) - -#define ZEND_ATOMIC_BOOL_INITIALIZER(desired) {.value = (desired)} -#define ZEND_ATOMIC_INT_INITIALIZER(desired) {.value = (desired)} - -static zend_always_inline bool zend_atomic_bool_exchange_ex(zend_atomic_bool *obj, bool desired) { - return InterlockedExchange8(&obj->value, desired); -} - -static zend_always_inline int zend_atomic_int_exchange_ex(zend_atomic_int *obj, int desired) { - return (int) InterlockedExchange(&obj->value, desired); -} - -static zend_always_inline bool zend_atomic_bool_compare_exchange_ex(zend_atomic_bool *obj, bool *expected, bool desired) { - bool prev = (bool) InterlockedCompareExchange8(&obj->value, *expected, desired); - if (prev == *expected) { - return true; - } else { - *expected = prev; - return false; - } -} - -static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_int *obj, int *expected, int desired) { - int prev = (int) InterlockedCompareExchange(&obj->value, *expected, desired); - if (prev == *expected) { - return true; - } else { - *expected = prev; - return false; - } -} - -/* On this platform it is non-const due to Interlocked API */ -static zend_always_inline bool zend_atomic_bool_load_ex(zend_atomic_bool *obj) { - /* Or'ing with false won't change the value. */ - return InterlockedOr8(&obj->value, false); -} - -static zend_always_inline int zend_atomic_int_load_ex(zend_atomic_int *obj) { - /* Or'ing with 0 won't change the value. */ - return (int) InterlockedOr(&obj->value, 0); -} - -static zend_always_inline void zend_atomic_bool_store_ex(zend_atomic_bool *obj, bool desired) { - (void)InterlockedExchange8(&obj->value, desired); -} - -static zend_always_inline void zend_atomic_int_store_ex(zend_atomic_int *obj, int desired) { - (void)InterlockedExchange(&obj->value, desired); -} - -#elif defined(HAVE_C11_ATOMICS) - -#define ZEND_ATOMIC_BOOL_INIT(obj, desired) __c11_atomic_init(&(obj)->value, (desired)) -#define ZEND_ATOMIC_INT_INIT(obj, desired) __c11_atomic_init(&(obj)->value, (desired)) - -#define ZEND_ATOMIC_BOOL_INITIALIZER(desired) {.value = (desired)} -#define ZEND_ATOMIC_INT_INITIALIZER(desired) {.value = (desired)} - -static zend_always_inline bool zend_atomic_bool_exchange_ex(zend_atomic_bool *obj, bool desired) { - return __c11_atomic_exchange(&obj->value, desired, __ATOMIC_SEQ_CST); -} - -static zend_always_inline int zend_atomic_int_exchange_ex(zend_atomic_int *obj, int desired) { - return __c11_atomic_exchange(&obj->value, desired, __ATOMIC_SEQ_CST); -} - -static zend_always_inline bool zend_atomic_bool_compare_exchange_ex(zend_atomic_bool *obj, bool *expected, bool desired) { - return __c11_atomic_compare_exchange_strong(&obj->value, expected, desired, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); -} - -static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_int *obj, int *expected, int desired) { - return __c11_atomic_compare_exchange_strong(&obj->value, expected, desired, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); -} - -static zend_always_inline bool zend_atomic_bool_load_ex(const zend_atomic_bool *obj) { - return __c11_atomic_load(&obj->value, __ATOMIC_SEQ_CST); -} - -static zend_always_inline int zend_atomic_int_load_ex(const zend_atomic_int *obj) { - return __c11_atomic_load(&obj->value, __ATOMIC_SEQ_CST); -} - -static zend_always_inline void zend_atomic_bool_store_ex(zend_atomic_bool *obj, bool desired) { - __c11_atomic_store(&obj->value, desired, __ATOMIC_SEQ_CST); -} - -static zend_always_inline void zend_atomic_int_store_ex(zend_atomic_int *obj, int desired) { - __c11_atomic_store(&obj->value, desired, __ATOMIC_SEQ_CST); -} - -#elif defined(HAVE_GNUC_ATOMICS) - -/* bool */ - -#define ZEND_ATOMIC_BOOL_INIT(obj, desired) ((obj)->value = (desired)) -#define ZEND_ATOMIC_INT_INIT(obj, desired) ((obj)->value = (desired)) - -#define ZEND_ATOMIC_BOOL_INITIALIZER(desired) {.value = (desired)} -#define ZEND_ATOMIC_INT_INITIALIZER(desired) {.value = (desired)} - -static zend_always_inline bool zend_atomic_bool_exchange_ex(zend_atomic_bool *obj, bool desired) { - bool prev = false; - __atomic_exchange(&obj->value, &desired, &prev, __ATOMIC_SEQ_CST); - return prev; -} - -static zend_always_inline int zend_atomic_int_exchange_ex(zend_atomic_int *obj, int desired) { - int prev = false; - __atomic_exchange(&obj->value, &desired, &prev, __ATOMIC_SEQ_CST); - return prev; -} - -static zend_always_inline bool zend_atomic_bool_compare_exchange_ex(zend_atomic_bool *obj, bool *expected, bool desired) { - return __atomic_compare_exchange(&obj->value, expected, &desired, /* weak */ false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); -} - -static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_int *obj, int *expected, int desired) { - return __atomic_compare_exchange(&obj->value, expected, &desired, /* weak */ false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); -} - -static zend_always_inline bool zend_atomic_bool_load_ex(const zend_atomic_bool *obj) { - bool prev = false; - __atomic_load(&obj->value, &prev, __ATOMIC_SEQ_CST); - return prev; -} - -static zend_always_inline int zend_atomic_int_load_ex(const zend_atomic_int *obj) { - int prev = false; - __atomic_load(&obj->value, &prev, __ATOMIC_SEQ_CST); - return prev; -} - -static zend_always_inline void zend_atomic_bool_store_ex(zend_atomic_bool *obj, bool desired) { - __atomic_store(&obj->value, &desired, __ATOMIC_SEQ_CST); -} - -static zend_always_inline void zend_atomic_int_store_ex(zend_atomic_int *obj, int desired) { - __atomic_store(&obj->value, &desired, __ATOMIC_SEQ_CST); -} - -#elif defined(HAVE_SYNC_ATOMICS) - -#define ZEND_ATOMIC_BOOL_INIT(obj, desired) ((obj)->value = (desired)) -#define ZEND_ATOMIC_INT_INIT(obj, desired) ((obj)->value = (desired)) - -#define ZEND_ATOMIC_BOOL_INITIALIZER(desired) {.value = (desired)} -#define ZEND_ATOMIC_INT_INITIALIZER(desired) {.value = (desired)} - -static zend_always_inline bool zend_atomic_bool_exchange_ex(zend_atomic_bool *obj, bool desired) { - bool prev = __sync_lock_test_and_set(&obj->value, desired); - - /* __sync_lock_test_and_set only does an acquire barrier, so sync - * immediately after. - */ - __sync_synchronize(); - return prev; -} - -static zend_always_inline int zend_atomic_int_exchange_ex(zend_atomic_int *obj, int desired) { - int prev = __sync_lock_test_and_set(&obj->value, desired); - - /* __sync_lock_test_and_set only does an acquire barrier, so sync - * immediately after. - */ - __sync_synchronize(); - return prev; -} - -static zend_always_inline bool zend_atomic_bool_compare_exchange_ex(zend_atomic_bool *obj, bool *expected, bool desired) { - bool prev = __sync_val_compare_and_swap(&obj->value, *expected, desired); - if (prev == *expected) { - return true; - } else { - *expected = prev; - return false; - } -} - -static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_int *obj, int *expected, int desired) { - int prev = __sync_val_compare_and_swap(&obj->value, *expected, desired); - if (prev == *expected) { - return true; - } else { - *expected = prev; - return false; - } -} - -static zend_always_inline bool zend_atomic_bool_load_ex(zend_atomic_bool *obj) { - /* Or'ing false won't change the value */ - return __sync_fetch_and_or(&obj->value, false); -} - -static zend_always_inline int zend_atomic_int_load_ex(zend_atomic_int *obj) { - /* Or'ing 0 won't change the value */ - return __sync_fetch_and_or(&obj->value, 0); -} - -static zend_always_inline void zend_atomic_bool_store_ex(zend_atomic_bool *obj, bool desired) { - __sync_synchronize(); - obj->value = desired; - __sync_synchronize(); -} - -static zend_always_inline void zend_atomic_int_store_ex(zend_atomic_int *obj, int desired) { - __sync_synchronize(); - obj->value = desired; - __sync_synchronize(); -} - -#elif defined(HAVE_NO_ATOMICS) - -#warning No atomics support detected. Please open an issue with platform details. - -#define ZEND_ATOMIC_BOOL_INIT(obj, desired) ((obj)->value = (desired)) -#define ZEND_ATOMIC_INT_INIT(obj, desired) ((obj)->value = (desired)) - -#define ZEND_ATOMIC_BOOL_INITIALIZER(desired) {.value = (desired)} -#define ZEND_ATOMIC_INT_INITIALIZER(desired) {.value = (desired)} - -static zend_always_inline void zend_atomic_bool_store_ex(zend_atomic_bool *obj, bool desired) { - obj->value = desired; -} - -static zend_always_inline void zend_atomic_int_store_ex(zend_atomic_int *obj, int desired) { - obj->value = desired; -} - -static zend_always_inline bool zend_atomic_bool_compare_exchange_ex(zend_atomic_int *obj, bool *expected, bool desired) { - bool prev = obj->value; - if (prev == *expected) { - obj->value = desired; - return true; - } else { - *expected = prev; - return false; - } -} - -static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_int *obj, int *expected, int desired) { - int prev = obj->value; - if (prev == *expected) { - obj->value = desired; - return true; - } else { - *expected = prev; - return false; - } -} - -static zend_always_inline bool zend_atomic_bool_load_ex(const zend_atomic_bool *obj) { - return obj->value; -} - -static zend_always_inline int zend_atomic_int_load_ex(const zend_atomic_int *obj) { - return obj->value; -} - -static zend_always_inline bool zend_atomic_bool_exchange_ex(zend_atomic_bool *obj, bool desired) { - bool prev = obj->value; - obj->value = desired; - return prev; -} - -static zend_always_inline int zend_atomic_int_exchange_ex(zend_atomic_int *obj, int desired) { - int prev = obj->value; - obj->value = desired; - return prev; -} - -#endif - -ZEND_API void zend_atomic_bool_init(zend_atomic_bool *obj, bool desired); -ZEND_API void zend_atomic_int_init(zend_atomic_int *obj, int desired); - -ZEND_API bool zend_atomic_bool_exchange(zend_atomic_bool *obj, bool desired); -ZEND_API int zend_atomic_int_exchange(zend_atomic_int *obj, int desired); - -ZEND_API bool zend_atomic_bool_compare_exchange(zend_atomic_bool *obj, bool *expected, bool desired); -ZEND_API bool zend_atomic_int_compare_exchange(zend_atomic_int *obj, int *expected, int desired); - -ZEND_API void zend_atomic_bool_store(zend_atomic_bool *obj, bool desired); -ZEND_API void zend_atomic_int_store(zend_atomic_int *obj, int desired); - -#if (defined(ZEND_WIN32) && !defined(HAVE_C11_ATOMICS)) || defined(HAVE_SYNC_ATOMICS) -/* On these platforms it is non-const due to underlying APIs. */ -ZEND_API bool zend_atomic_bool_load(zend_atomic_bool *obj); -ZEND_API int zend_atomic_int_load(zend_atomic_int *obj); -#else -ZEND_API bool zend_atomic_bool_load(const zend_atomic_bool *obj); -ZEND_API int zend_atomic_int_load(const zend_atomic_int *obj); -#endif - -#if defined(HAVE_C11_ATOMICS) -# define ZEND_ATOMIC_FENCE_RELEASE() __c11_atomic_thread_fence(__ATOMIC_RELEASE) -# define ZEND_ATOMIC_FENCE_ACQUIRE() __c11_atomic_thread_fence(__ATOMIC_ACQUIRE) -#elif defined(HAVE_GNUC_ATOMICS) -# define ZEND_ATOMIC_FENCE_RELEASE() __atomic_thread_fence(__ATOMIC_RELEASE) -# define ZEND_ATOMIC_FENCE_ACQUIRE() __atomic_thread_fence(__ATOMIC_ACQUIRE) -#elif defined(HAVE_SYNC_ATOMICS) -# define ZEND_ATOMIC_FENCE_RELEASE() __sync_synchronize() -# define ZEND_ATOMIC_FENCE_ACQUIRE() __sync_synchronize() -#elif defined(ZEND_WIN32) -# define ZEND_ATOMIC_FENCE_RELEASE() MemoryBarrier() -# define ZEND_ATOMIC_FENCE_ACQUIRE() MemoryBarrier() -#else -# define ZEND_ATOMIC_FENCE_RELEASE() ((void)0) -# define ZEND_ATOMIC_FENCE_ACQUIRE() ((void)0) -#endif - -END_EXTERN_C() - -#endif diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index a5f9d1e8c848..ff3a5f0b3de9 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4344,8 +4344,8 @@ ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *exec ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *call) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), false); - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + atomic_store(&EG(vm_interrupt), false); + if (atomic_load(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { zend_interrupt_function(call); @@ -4353,7 +4353,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *ca } #define ZEND_VM_INTERRUPT_CHECK() do { \ - if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \ + if (UNEXPECTED(atomic_load(&EG(vm_interrupt)))) { \ ZEND_VM_INTERRUPT(); \ } \ } while (0) @@ -4365,14 +4365,14 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *ca #endif #define ZEND_VM_LOOP_INTERRUPT_CHECK() do { \ - if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \ + if (UNEXPECTED(atomic_load(&EG(vm_interrupt)))) { \ ZEND_VM_KIND_TAILCALL_SAVE_OPLINE(); \ ZEND_VM_LOOP_INTERRUPT(); \ } \ } while (0) #define ZEND_VM_FCALL_INTERRUPT_CHECK(call) do { \ - if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \ + if (UNEXPECTED(atomic_load(&EG(vm_interrupt)))) { \ zend_fcall_interrupt(call); \ } \ } while (0) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 900487cf3898..7910c840a17b 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -172,8 +172,8 @@ void init_executor(void) /* {{{ */ zend_lazy_objects_init(&EG(lazy_objects_store)); EG(full_tables_cleanup) = 0; - ZEND_ATOMIC_BOOL_INIT(&EG(vm_interrupt), false); - ZEND_ATOMIC_BOOL_INIT(&EG(timed_out), false); + atomic_init(&EG(vm_interrupt), false); + atomic_init(&EG(timed_out), false); EG(exception) = NULL; @@ -1099,8 +1099,8 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ /* This flag is regularly checked while running user functions, but not internal * So see whether interrupt flag was set while the function was running... */ - if (zend_atomic_bool_exchange_ex(&EG(vm_interrupt), false)) { - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_exchange(&EG(vm_interrupt), false)) { + if (atomic_load(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { zend_interrupt_function(EG(current_execute_data)); @@ -1476,14 +1476,14 @@ ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void) /* {{{ */ timer is not restarted properly, it could hang in the shutdown function. */ if (EG(hard_timeout) > 0) { - zend_atomic_bool_store_ex(&EG(timed_out), false); + atomic_store(&EG(timed_out), false); zend_set_timeout_ex(EG(hard_timeout), true); /* XXX Abused, introduce an additional flag if the value needs to be kept. */ EG(hard_timeout) = 0; } # endif #else - zend_atomic_bool_store_ex(&EG(timed_out), false); + atomic_store(&EG(timed_out), false); zend_set_timeout_ex(0, true); #endif @@ -1528,7 +1528,7 @@ static void zend_timeout_handler(int dummy) /* {{{ */ return; } #else - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_load(&EG(timed_out))) { /* Die on hard timeout */ const char *error_filename = NULL; uint32_t error_lineno = 0; @@ -1563,8 +1563,8 @@ static void zend_timeout_handler(int dummy) /* {{{ */ zend_on_timeout(EG(timeout_seconds)); } - zend_atomic_bool_store_ex(&EG(timed_out), true); - zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + atomic_store(&EG(timed_out), true); + atomic_store(&EG(vm_interrupt), true); #ifndef ZTS if (EG(hard_timeout) > 0) { @@ -1588,8 +1588,8 @@ VOID CALLBACK tq_timer_cb(PVOID arg, BOOLEAN timed_out) } eg = (zend_executor_globals *)arg; - zend_atomic_bool_store_ex(&eg->timed_out, true); - zend_atomic_bool_store_ex(&eg->vm_interrupt, true); + atomic_store(&eg->timed_out, true); + atomic_store(&eg->vm_interrupt, true); } #endif @@ -1697,7 +1697,7 @@ void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */ EG(timeout_seconds) = seconds; zend_set_timeout_ex(seconds, reset_signals); - zend_atomic_bool_store_ex(&EG(timed_out), false); + atomic_store(&EG(timed_out), false); } /* }}} */ @@ -1706,7 +1706,7 @@ void zend_unset_timeout(void) /* {{{ */ #ifdef ZEND_WIN32 if (NULL != tq_timer) { if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) { - zend_atomic_bool_store_ex(&EG(timed_out), false); + atomic_store(&EG(timed_out), false); tq_timer = NULL; zend_error_noreturn(E_ERROR, "Could not delete queued timer"); } @@ -1729,7 +1729,7 @@ void zend_unset_timeout(void) /* {{{ */ # endif } #endif - zend_atomic_bool_store_ex(&EG(timed_out), false); + atomic_store(&EG(timed_out), false); } /* }}} */ diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index b7835ed63226..d74bd8e63447 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -26,7 +26,6 @@ #include "zend_globals_macros.h" -#include "zend_atomic.h" #include "zend_stack.h" #include "zend_ptr_stack.h" #include "zend_hash.h" @@ -223,8 +222,8 @@ struct _zend_executor_globals { bool full_tables_cleanup; - zend_atomic_bool vm_interrupt; - zend_atomic_bool timed_out; + atomic_bool vm_interrupt; + atomic_bool timed_out; HashTable autoload_current_classnames; diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h index 0ffd8baa2cfa..a3028efe172b 100644 --- a/Zend/zend_portability.h +++ b/Zend/zend_portability.h @@ -52,6 +52,23 @@ #include #include +#ifdef __cplusplus +extern "C++" { +# include +/* Make the atomic types used by Zend available to C++ extensions. */ +using std::atomic_bool; +using std::atomic_int; +} +#else +# if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L +# error "Zend requires C11 or later" +# endif +# ifdef __STDC_NO_ATOMICS__ +# error "Zend requires C11 atomics" +# endif +# include +#endif + #ifdef HAVE_UNIX_H # include #endif diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index c0fd2eef277b..be69d2624cfd 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10711,14 +10711,14 @@ ZEND_VM_DEFINE_OP(137, ZEND_OP_DATA); ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), false); + atomic_store(&EG(vm_interrupt), false); #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL /* opline is &call_interrupt_op. Load orig opline. */ LOAD_OPLINE(); #else SAVE_OPLINE(); #endif - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_load(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { zend_interrupt_function(execute_data); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index b956b638b7e4..5e5677a9f0b1 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4032,14 +4032,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_J static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), false); + atomic_store(&EG(vm_interrupt), false); #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL /* opline is &call_interrupt_op. Load orig opline. */ LOAD_OPLINE(); #else SAVE_OPLINE(); #endif - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_load(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { zend_interrupt_function(execute_data); @@ -56877,14 +56877,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_JMP_FO static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), false); + atomic_store(&EG(vm_interrupt), false); #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL /* opline is &call_interrupt_op. Load orig opline. */ LOAD_OPLINE(); #else SAVE_OPLINE(); #endif - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_load(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { zend_interrupt_function(execute_data); diff --git a/configure.ac b/configure.ac index 50df06a6b479..f48f1e8de90e 100644 --- a/configure.ac +++ b/configure.ac @@ -1757,7 +1757,6 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ zend_alloc.c zend_API.c zend_ast.c - zend_atomic.c zend_attributes.c zend_autoload.c zend_builtin_functions.c diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 9d6167ba73e3..4e4c815377ac 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -2541,7 +2541,7 @@ static zend_class_entry* zend_accel_inheritance_cache_get(zend_class_entry *ce, zend_inheritance_cache_entry *entry = ce->inheritance_cache; if (entry) { - ZEND_ATOMIC_FENCE_ACQUIRE(); + atomic_thread_fence(memory_order_acquire); } while (entry) { @@ -2702,7 +2702,7 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce, entry->next = proto->inheritance_cache; ZCSG(map_ptr_last) = CG(map_ptr_last); - ZEND_ATOMIC_FENCE_RELEASE(); + atomic_thread_fence(memory_order_release); proto->inheritance_cache = entry; zend_shared_alloc_destroy_xlat_table(); diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 45936ba01f48..21c15630bdf8 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -8836,7 +8836,7 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf EX(opline) = opline; } - if (zend_atomic_bool_load_ex(&EG(vm_interrupt)) || JIT_G(tracing)) { + if (atomic_load(&EG(vm_interrupt)) || JIT_G(tracing)) { return 1; /* Lock-free check if the side trace was already JIT-ed or blacklist-ed in another process */ } else if (t->exit_info[exit_num].flags & (ZEND_JIT_EXIT_JITED|ZEND_JIT_EXIT_BLACKLISTED)) { diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 0646e7ba043f..3a4d8ee7e25c 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -1345,7 +1345,7 @@ static void pcntl_signal_handler(int signo, siginfo_t *siginfo, void *context) PCNTL_G(tail) = psig; PCNTL_G(pending_signals) = true; if (PCNTL_G(async_signals)) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + atomic_store(&EG(vm_interrupt), true); } } @@ -1453,7 +1453,7 @@ void pcntl_signal_dispatch(void) PCNTL_G(tail) = next; if (PCNTL_G(async_signals)) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + atomic_store(&EG(vm_interrupt), true); } } else { PCNTL_G(pending_signals) = false; diff --git a/ext/random/csprng.c b/ext/random/csprng.c index 4c474fb86796..b6333aaea284 100644 --- a/ext/random/csprng.c +++ b/ext/random/csprng.c @@ -24,7 +24,6 @@ #include "php.h" #include "Zend/zend_exceptions.h" -#include "Zend/zend_atomic.h" #include "php_random.h" #include "php_random_csprng.h" @@ -61,7 +60,7 @@ #endif #ifndef PHP_WIN32 -static zend_atomic_int random_fd = ZEND_ATOMIC_INT_INITIALIZER(-1); +static atomic_int random_fd = -1; #endif ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes_ex(void *bytes, size_t size, char *errstr, size_t errstr_size) @@ -146,7 +145,7 @@ ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes_ex(void *bytes, size_ } # endif if (read_bytes < size) { - int fd = zend_atomic_int_load_ex(&random_fd); + int fd = atomic_load(&random_fd); struct stat st; if (fd < 0) { @@ -179,7 +178,7 @@ ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes_ex(void *bytes, size_ return FAILURE; } int expected = -1; - if (!zend_atomic_int_compare_exchange_ex(&random_fd, &expected, fd)) { + if (!atomic_compare_exchange_strong(&random_fd, &expected, fd)) { close(fd); /* expected is now the actual value of random_fd */ fd = expected; @@ -265,7 +264,7 @@ ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_int(zend_long min, zend_lon PHPAPI void php_random_csprng_shutdown(void) { #ifndef PHP_WIN32 - int fd = zend_atomic_int_exchange(&random_fd, -1); + int fd = atomic_exchange(&random_fd, -1); if (fd != -1) { close(fd); } diff --git a/ext/zend_test/object_handlers.c b/ext/zend_test/object_handlers.c index 6b8bf4ab9912..9e78e9fdf524 100644 --- a/ext/zend_test/object_handlers.c +++ b/ext/zend_test/object_handlers.c @@ -250,7 +250,7 @@ static int vm_interrupt_comparable_compare(zval *op1, zval *op2) { ZEND_COMPARE_OBJECTS_FALLBACK(op1, op2); - zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + atomic_store(&EG(vm_interrupt), true); return ZEND_THREEWAY_COMPARE( Z_LVAL_P(OBJ_PROP_NUM(Z_OBJ_P(op1), 0)), diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index 2fd4073b4af0..77ce0cc2525e 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -76,7 +76,7 @@ static void observer_begin(zend_execute_data *execute_data) assert_observer_opline(execute_data); if (ZT_G(observer_set_vm_interrupt_on_begin)) { - zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + atomic_store(&EG(vm_interrupt), true); } if (!ZT_G(observer_show_output)) { diff --git a/sapi/phpdbg/phpdbg_prompt.c b/sapi/phpdbg/phpdbg_prompt.c index 88afd1b3752e..b879cde43653 100644 --- a/sapi/phpdbg/phpdbg_prompt.c +++ b/sapi/phpdbg/phpdbg_prompt.c @@ -1652,7 +1652,7 @@ void phpdbg_execute_ex(zend_execute_data *execute_data) /* {{{ */ } #ifdef ZEND_WIN32 - if (zend_atomic_bool_load_ex(&EG(timed_out))) { + if (atomic_load(&EG(timed_out))) { zend_timeout(); } #endif diff --git a/win32/build/config.w32 b/win32/build/config.w32 index f352d4c794aa..253dab653986 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -240,7 +240,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \ zend_default_classes.c zend_execute.c zend_strtod.c zend_gc.c zend_closures.c zend_weakrefs.c \ zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \ zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \ - zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \ + zend_enum.c zend_fibers.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \ zend_lazy_objects.c zend_autoload.c zend_partial.c"); ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c"); diff --git a/win32/signal.c b/win32/signal.c index abce3edb2fd1..1cf917d595a0 100644 --- a/win32/signal.c +++ b/win32/signal.c @@ -20,7 +20,7 @@ /* true globals; only used from main thread and from kernel callback */ static zend_fcall_info_cache ctrl_handler; static DWORD ctrl_evt = (DWORD)-1; -static zend_atomic_bool *vm_interrupt_flag = NULL; +static atomic_bool *vm_interrupt_flag = NULL; static void (*orig_interrupt_function)(zend_execute_data *execute_data); @@ -91,7 +91,7 @@ static BOOL WINAPI php_win32_signal_system_ctrl_handler(DWORD evt) return FALSE; } - zend_atomic_bool_store_ex(vm_interrupt_flag, true); + atomic_store(vm_interrupt_flag, true); ctrl_evt = evt; From d2208a22b33920cf68b2b23a5cab7077a917e7e5 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 29 Sep 2026 16:44:37 +0200 Subject: [PATCH 04/23] tests: Raise test stack for stream error depth limit under MSan (#23985) --- .../stream_errors_operation_depth_limit.phpt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt b/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt index 1eae9c5fd31d..3c1f934ef30d 100644 --- a/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt +++ b/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt @@ -2,8 +2,30 @@ Stream errors: operations refused by the depth limit keep the stack consistent --INI-- zend.max_allowed_stack_size=-1 +--SKIPIF-- + --FILE-- = 32 * 1024 * 1024)) { + posix_setrlimit(POSIX_RLIMIT_STACK, 32 * 1024 * 1024, $hard === 'unlimited' ? -1 : $hard); + } +} + class RecursiveStream { public $context; From 22237f2c07c5260c2c0da7a5b002ac03ecb71473 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 26 Sep 2026 18:15:07 -0400 Subject: [PATCH 05/23] ext/pdo: Release driver options after bindParam and bindColumn bindParam() and bindColumn() copied the driver options zval and then addref'd it again. The extra reference kept the value alive after the statement was destroyed. Keep the single reference from the copy. Closes GH-23936 --- NEWS | 2 ++ ext/pdo/pdo_stmt.c | 8 ++--- .../tests/pdo_driver_options_weakref.phpt | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt diff --git a/NEWS b/NEWS index 85b41dfb7cae..8eb6798b7008 100644 --- a/NEWS +++ b/NEWS @@ -106,6 +106,8 @@ PHP NEWS whose constructor arguments it rejects. (Ilia Alshanetsky) . Fixed PDO::ATTR_STATEMENT_CLASS constructor_args type errors reporting "array given" regardless of the value passed. (Ilia Alshanetsky) + . Fixed PDOStatement::bindParam() and bindColumn() leaking the driver + options value. (Ilia Alshanetsky) - PDO_Firebird: . Fixed bug GH-23758 (PDO_Firebird returns null for non-null empty BLOBs). diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 97d1a058fd52..c4abbfe45362 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -287,10 +287,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ param->stmt = stmt; param->is_param = is_param; - if (Z_REFCOUNTED(param->driver_params)) { - Z_ADDREF(param->driver_params); - } - if (!is_param && param->name && stmt->columns) { /* try to map the name to the column */ int i; @@ -374,6 +370,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ } else { zend_hash_index_del(hash, pparam->paramno); } + ZVAL_UNDEF(¶m->driver_params); /* param->parameter is freed by hash dtor */ ZVAL_UNDEF(¶m->parameter); return 0; @@ -1462,6 +1459,9 @@ static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /* if (!Z_ISUNDEF(param.parameter)) { zval_ptr_dtor(&(param.parameter)); } + if (!Z_ISUNDEF(param.driver_params)) { + zval_ptr_dtor(¶m.driver_params); + } RETURN_FALSE; } diff --git a/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt new file mode 100644 index 000000000000..16dc9a3c4666 --- /dev/null +++ b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt @@ -0,0 +1,35 @@ +--TEST-- +PDO SQLite releases driverOptions objects after binding or failed registration +--EXTENSIONS-- +pdo_sqlite +--FILE-- +prepare('SELECT ? AS value'); +$value = 1; +$driverOptions = [new Tracked()]; +$weakReference = WeakReference::create($driverOptions[0]); +var_dump($stmt->bindParam(1, $value, PDO::PARAM_STR, 0, $driverOptions)); +unset($driverOptions, $value, $stmt); +gc_collect_cycles(); +var_dump($weakReference->get()); + +$stmt = $db->prepare('SELECT ? AS value'); +$stmt->execute(); +$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$value = null; +$driverOptions = [new Tracked()]; +$weakReference = WeakReference::create($driverOptions[0]); +var_dump(@$stmt->bindColumn('missing', $value, PDO::PARAM_STR, 0, $driverOptions)); +unset($driverOptions); +var_dump($weakReference->get()); +unset($value, $stmt); +?> +--EXPECT-- +bool(true) +NULL +bool(false) +NULL From 3884053c04ccca6597657975342c4ea819890206 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 29 Sep 2026 16:20:29 +0100 Subject: [PATCH 06/23] Zend: rename zend_object* parameter to "this_ptr" for zend_call_* functions (#23989) To be more explicit in what the zend_object* represents. --- Zend/zend_API.c | 28 ++++++++++++++-------------- Zend/zend_API.h | 26 +++++++++++++------------- Zend/zend_execute_API.c | 17 +++++++++-------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 9b865b7b5d28..db20926dd96d 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3791,13 +3791,13 @@ static bool zend_is_callable_check_class(zend_string *name, zend_class_entry *sc const zend_class_entry *frame_scope = get_scope(frame); fcc->calling_scope = ce; if (frame_scope && !fcc->object) { - zend_object *object = zend_get_this_object(frame); + zend_object *this_ptr = zend_get_this_object(frame); - if (object && - instanceof_function(object->ce, frame_scope) && + if (this_ptr && + instanceof_function(this_ptr->ce, frame_scope) && instanceof_function(frame_scope, ce)) { - fcc->object = object; - fcc->called_scope = object->ce; + fcc->object = this_ptr; + fcc->called_scope = this_ptr->ce; } else { fcc->called_scope = ce; } @@ -3870,13 +3870,13 @@ static zend_always_inline bool zend_is_method_callable(zend_string *callable, co if (ZSTR_HAS_CE_CACHE(class_name) && ZSTR_GET_CE_CACHE(class_name)) { fcc->calling_scope = ZSTR_GET_CE_CACHE(class_name); if (scope && !fcc->object) { - zend_object *object = zend_get_this_object(frame); + zend_object *this_ptr = zend_get_this_object(frame); - if (object && - instanceof_function(object->ce, scope) && + if (this_ptr && + instanceof_function(this_ptr->ce, scope) && instanceof_function(scope, fcc->calling_scope)) { - fcc->object = object; - fcc->called_scope = object->ce; + fcc->object = this_ptr; + fcc->called_scope = this_ptr->ce; } else { fcc->called_scope = fcc->calling_scope; } @@ -3982,10 +3982,10 @@ static zend_always_inline bool zend_is_method_callable(zend_string *callable, co retval = true; call_via_handler = (fcc->function_handler->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) != 0; if (call_via_handler && !fcc->object) { - zend_object *object = zend_get_this_object(frame); - if (object && - instanceof_function(object->ce, fcc->calling_scope)) { - fcc->object = object; + zend_object *this_ptr = zend_get_this_object(frame); + if (this_ptr && + instanceof_function(this_ptr->ce, fcc->calling_scope)) { + fcc->object = this_ptr; } } } diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 786ccafbb9d0..5c4083052202 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -60,7 +60,7 @@ typedef struct _zend_fcall_info_cache { zend_function *function_handler; zend_class_entry *calling_scope; zend_class_entry *called_scope; - zend_object *object; /* Instance of object for method calls */ + zend_object *object; /* Pointer for object representing $this */ zend_object *closure; /* Closure reference, only if the callable *is* the object */ } zend_fcall_info_cache; @@ -858,16 +858,16 @@ static zend_always_inline zend_result zend_call_function_with_return_value( /* Call the provided zend_function with the given params. * If retval_ptr is NULL, the return value is discarded. - * If object is NULL, this must be a free function or static call. + * If this_ptr is NULL, this must be a free function or static call. * called_scope must be provided for instance and static method calls. */ ZEND_API void zend_call_known_function_ex( - zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr, + zend_function *fn, zend_object *this_ptr, zend_class_entry *called_scope, zval *retval_ptr, uint32_t param_count, zval *params, HashTable *named_params, uint32_t consumed_args); static zend_always_inline void zend_call_known_function( - zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr, + zend_function *fn, zend_object *this_ptr, zend_class_entry *called_scope, zval *retval_ptr, uint32_t param_count, zval *params, HashTable *named_params) { - zend_call_known_function_ex(fn, object, called_scope, retval_ptr, param_count, params, named_params, 0); + zend_call_known_function_ex(fn, this_ptr, called_scope, retval_ptr, param_count, params, named_params, 0); } static zend_always_inline void zend_call_known_fcc_ex( @@ -892,32 +892,32 @@ static zend_always_inline void zend_call_known_fcc( /* Call the provided zend_function instance method on an object. */ static zend_always_inline void zend_call_known_instance_method( - zend_function *fn, zend_object *object, zval *retval_ptr, + zend_function *fn, zend_object *this_ptr, zval *retval_ptr, uint32_t param_count, zval *params) { - zend_call_known_function(fn, object, object->ce, retval_ptr, param_count, params, NULL); + zend_call_known_function(fn, this_ptr, this_ptr->ce, retval_ptr, param_count, params, NULL); } static zend_always_inline void zend_call_known_instance_method_with_0_params( - zend_function *fn, zend_object *object, zval *retval_ptr) + zend_function *fn, zend_object *this_ptr, zval *retval_ptr) { - zend_call_known_instance_method(fn, object, retval_ptr, 0, NULL); + zend_call_known_instance_method(fn, this_ptr, retval_ptr, 0, NULL); } static zend_always_inline void zend_call_known_instance_method_with_1_params( - zend_function *fn, zend_object *object, zval *retval_ptr, zval *param) + zend_function *fn, zend_object *this_ptr, zval *retval_ptr, zval *param) { - zend_call_known_instance_method(fn, object, retval_ptr, 1, param); + zend_call_known_instance_method(fn, this_ptr, retval_ptr, 1, param); } ZEND_API void zend_call_known_instance_method_with_2_params( - zend_function *fn, zend_object *object, zval *retval_ptr, zval *param1, zval *param2); + zend_function *fn, zend_object *this_ptr, zval *retval_ptr, zval *param1, zval *param2); /* Call method if it exists. Return FAILURE if method does not exist or call failed. * If FAILURE is returned, retval will be UNDEF. As such, destroying retval unconditionally * is legal. */ ZEND_API zend_result zend_call_method_if_exists( - zend_object *object, zend_string *method_name, zval *retval, + zend_object *this_ptr, zend_string *method_name, zval *retval, uint32_t param_count, zval *params); ZEND_API zend_result zend_delete_global_variable(zend_string *name); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 7910c840a17b..3d0b690944b4 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1133,7 +1133,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ /* }}} */ ZEND_API void zend_call_known_function_ex( - zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr, + zend_function *fn, zend_object *this_ptr, zend_class_entry *called_scope, zval *retval_ptr, uint32_t param_count, zval *params, HashTable *named_params, uint32_t consumed_args) { zval retval; @@ -1143,16 +1143,17 @@ ZEND_API void zend_call_known_function_ex( ZEND_ASSERT(fn && "zend_function must be passed!"); fci.size = sizeof(fci); - fci.object = object; fci.retval = retval_ptr ? retval_ptr : &retval; fci.param_count = param_count; fci.params = params; fci.named_params = named_params; fci.consumed_args = consumed_args; - ZVAL_UNDEF(&fci.function_name); /* Unused */ + /* Unused */ + ZVAL_UNDEF(&fci.function_name); + fci.object = NULL; fcic.function_handler = fn; - fcic.object = object; + fcic.object = this_ptr; fcic.called_scope = called_scope; zend_result result = zend_call_function(&fci, &fcic); @@ -1170,16 +1171,16 @@ ZEND_API void zend_call_known_function_ex( } ZEND_API void zend_call_known_instance_method_with_2_params( - zend_function *fn, zend_object *object, zval *retval_ptr, zval *param1, zval *param2) + zend_function *fn, zend_object *this_ptr, zval *retval_ptr, zval *param1, zval *param2) { zval params[2]; ZVAL_COPY_VALUE(¶ms[0], param1); ZVAL_COPY_VALUE(¶ms[1], param2); - zend_call_known_instance_method(fn, object, retval_ptr, 2, params); + zend_call_known_instance_method(fn, this_ptr, retval_ptr, 2, params); } ZEND_API zend_result zend_call_method_if_exists( - zend_object *object, zend_string *method_name, zval *retval, + zend_object *this_ptr, zend_string *method_name, zval *retval, uint32_t param_count, zval *params) { zval zval_method; @@ -1187,7 +1188,7 @@ ZEND_API zend_result zend_call_method_if_exists( ZVAL_STR(&zval_method, method_name); - if (UNEXPECTED(!zend_is_callable_ex(&zval_method, object, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL))) { + if (UNEXPECTED(!zend_is_callable_ex(&zval_method, this_ptr, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL))) { ZVAL_UNDEF(retval); return FAILURE; } From 7f1e8cb37b1e6da736070d5501b06d2d1763ef12 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 29 Sep 2026 11:38:23 -0400 Subject: [PATCH 07/23] ext/pdo: Throw a ValueError from bindColumn() for an unknown column (#23835) A column name that is not in the result set is a programming error, so report it the way the method already reports an empty name or an index below one, rather than through PDO::ATTR_ERRMODE. The equivalent parameter failure in bindParam() and bindValue() stays an ERRMODE error: its site in rewrite_name_to_position() is only reachable once execute() has populated bound_param_map, never from the bind methods themselves. Closes GH-23835 --- UPGRADING | 5 ++++ ext/pdo/pdo_stmt.c | 7 +---- .../tests/pdo_bindcolumn_unknown_column.phpt | 30 +++++++++++-------- .../tests/pdo_driver_options_weakref.phpt | 11 ++++--- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/UPGRADING b/UPGRADING index 5d551005042e..27dc06f12d2e 100644 --- a/UPGRADING +++ b/UPGRADING @@ -19,6 +19,11 @@ PHP 8.7 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== +- PDO: + . PDOStatement::bindColumn() now throws a ValueError when the column name is + not present in the result set. It previously reported the condition + through PDO::ATTR_ERRMODE and returned false. + - Standard: . The number of filters that can be chained in a php://filter URL is limited to 16 by default. Set the stream context option max_filter_count to change diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 1e9e1052d282..9774b197b7d9 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -295,12 +295,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ /* if you prepare and then execute passing an array of params keyed by names, * then this will trigger, and we don't want that */ if (param->paramno == -1) { - /* Should this always be an Error? */ - char *tmp; - /* TODO Error? */ - spprintf(&tmp, 0, "Did not find column name '%s' in the defined columns; it will not be bound", ZSTR_VAL(param->name)); - pdo_raise_impl_error(stmt->dbh, stmt, "HY000", tmp); - efree(tmp); + zend_argument_value_error(1, "must refer to a column present in the result set, \"%s\" given", ZSTR_VAL(param->name)); return false; } } diff --git a/ext/pdo/tests/pdo_bindcolumn_unknown_column.phpt b/ext/pdo/tests/pdo_bindcolumn_unknown_column.phpt index 44e85ec07c31..3e3ba30e8c41 100644 --- a/ext/pdo/tests/pdo_bindcolumn_unknown_column.phpt +++ b/ext/pdo/tests/pdo_bindcolumn_unknown_column.phpt @@ -1,5 +1,5 @@ --TEST-- -PDO: bindColumn() must fail for a column name that is not in the result set +PDO: bindColumn() must throw for a column name that is not in the result set --EXTENSIONS-- pdo --SKIPIF-- @@ -17,16 +17,20 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); $db->exec('CREATE TABLE pdo_bindcolumn_unknown_column (name varchar(255))'); -$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); -$stmt = $db->query('SELECT name FROM pdo_bindcolumn_unknown_column'); -var_dump(@$stmt->bindColumn('nosuchcolumn', $var)); - -$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); -try { - $stmt->bindColumn('nosuchcolumn', $var); -} catch (PDOException $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; +// The error mode must not affect a ValueError. +foreach ([PDO::ERRMODE_SILENT, PDO::ERRMODE_WARNING, PDO::ERRMODE_EXCEPTION] as $mode) { + $db->setAttribute(PDO::ATTR_ERRMODE, $mode); + $stmt = $db->query('SELECT name FROM pdo_bindcolumn_unknown_column'); + try { + $stmt->bindColumn('nosuchcolumn', $var); + } catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } } + +// A column that does exist still binds. +$stmt = $db->query('SELECT name FROM pdo_bindcolumn_unknown_column'); +var_dump($stmt->bindColumn('name', $var)); ?> --CLEAN-- exec('DROP TABLE pdo_bindcolumn_unknown_column'); ?> --EXPECT-- -bool(false) -PDOException: SQLSTATE[HY000]: General error: Did not find column name 'nosuchcolumn' in the defined columns; it will not be bound +ValueError: PDOStatement::bindColumn(): Argument #1 ($column) must refer to a column present in the result set, "nosuchcolumn" given +ValueError: PDOStatement::bindColumn(): Argument #1 ($column) must refer to a column present in the result set, "nosuchcolumn" given +ValueError: PDOStatement::bindColumn(): Argument #1 ($column) must refer to a column present in the result set, "nosuchcolumn" given +bool(true) diff --git a/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt index 16dc9a3c4666..15cafa656e70 100644 --- a/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt +++ b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt @@ -19,17 +19,20 @@ var_dump($weakReference->get()); $stmt = $db->prepare('SELECT ? AS value'); $stmt->execute(); -$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); $value = null; $driverOptions = [new Tracked()]; $weakReference = WeakReference::create($driverOptions[0]); -var_dump(@$stmt->bindColumn('missing', $value, PDO::PARAM_STR, 0, $driverOptions)); -unset($driverOptions); +try { + $stmt->bindColumn('missing', $value, PDO::PARAM_STR, 0, $driverOptions); +} catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +unset($e, $driverOptions); var_dump($weakReference->get()); unset($value, $stmt); ?> --EXPECT-- bool(true) NULL -bool(false) +ValueError: PDOStatement::bindColumn(): Argument #1 ($column) must refer to a column present in the result set, "missing" given NULL From 68301c0df82c31a1e1f8edac42123695f0850033 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 30 Sep 2026 00:02:36 +0800 Subject: [PATCH 08/23] ext/standard: Remove redundant if-branch in rot13 (#23795) --- UPGRADING | 1 + ext/standard/string.c | 24 ++++++++---------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/UPGRADING b/UPGRADING index 27dc06f12d2e..10994928470a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -84,3 +84,4 @@ PHP 8.7 UPGRADE NOTES - Standard: . Improved performance of array_splice() when inserting without removing elements. + . Improved performance of str_rot13(). diff --git a/ext/standard/string.c b/ext/standard/string.c index ddd3f31bf14f..d23c117321a4 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -6054,34 +6054,26 @@ static zend_string *php_str_rot13(zend_string *str) gt = _mm_cmpgt_epi8(in, a_minus_1); lt = _mm_cmplt_epi8(in, m_plus_1); cmp = _mm_and_si128(lt, gt); - if (_mm_movemask_epi8(cmp)) { - cmp = _mm_and_si128(cmp, add); - delta = _mm_or_si128(delta, cmp); - } + cmp = _mm_and_si128(cmp, add); + delta = _mm_or_si128(delta, cmp); gt = _mm_cmpgt_epi8(in, n_minus_1); lt = _mm_cmplt_epi8(in, z_plus_1); cmp = _mm_and_si128(lt, gt); - if (_mm_movemask_epi8(cmp)) { - cmp = _mm_and_si128(cmp, sub); - delta = _mm_or_si128(delta, cmp); - } + cmp = _mm_and_si128(cmp, sub); + delta = _mm_or_si128(delta, cmp); gt = _mm_cmpgt_epi8(in, A_minus_1); lt = _mm_cmplt_epi8(in, M_plus_1); cmp = _mm_and_si128(lt, gt); - if (_mm_movemask_epi8(cmp)) { - cmp = _mm_and_si128(cmp, add); - delta = _mm_or_si128(delta, cmp); - } + cmp = _mm_and_si128(cmp, add); + delta = _mm_or_si128(delta, cmp); gt = _mm_cmpgt_epi8(in, N_minus_1); lt = _mm_cmplt_epi8(in, Z_plus_1); cmp = _mm_and_si128(lt, gt); - if (_mm_movemask_epi8(cmp)) { - cmp = _mm_and_si128(cmp, sub); - delta = _mm_or_si128(delta, cmp); - } + cmp = _mm_and_si128(cmp, sub); + delta = _mm_or_si128(delta, cmp); in = _mm_add_epi8(in, delta); _mm_storeu_si128((__m128i *)target, in); From 023ee4dc9983b036ccf889cd1cd70b0016716ef9 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 29 Sep 2026 21:02:57 +0500 Subject: [PATCH 09/23] Fix GH-23986: Clear the realpath cache in the child after pcntl_fork() (#23987) --- NEWS | 2 ++ ext/pcntl/pcntl.c | 4 ++++ ext/pcntl/tests/gh23986.phpt | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 ext/pcntl/tests/gh23986.phpt diff --git a/NEWS b/NEWS index 8eb6798b7008..4ace934375fd 100644 --- a/NEWS +++ b/NEWS @@ -94,6 +94,8 @@ PHP NEWS the inclusive upper bound. (lacatoire) . Fixed pcntl_setqos_class() requiring its optional $qos_class argument. (lacatoire) + . Fixed bug GH-23986 (/proc/self paths resolve to the parent process after + pcntl_fork()). (Lazizbek Ergashev) - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 34a670282636..1858ee6e4a72 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -29,6 +29,7 @@ #include "php.h" #include "ext/standard/info.h" +#include "ext/standard/php_filestat.h" #include "php_signal.h" #include "php_ticks.h" #include "zend_exceptions.h" @@ -294,6 +295,7 @@ PHP_FUNCTION(pcntl_fork) } } else if (id == 0) { zend_max_execution_timer_init(); + php_clear_stat_cache(true, NULL, 0); } RETURN_LONG((zend_long) id); @@ -1573,6 +1575,7 @@ PHP_FUNCTION(pcntl_rfork) } } else if (pid == 0) { zend_max_execution_timer_init(); + php_clear_stat_cache(true, NULL, 0); } RETURN_LONG((zend_long) pid); @@ -1618,6 +1621,7 @@ PHP_FUNCTION(pcntl_forkx) } } else if (pid == 0) { zend_max_execution_timer_init(); + php_clear_stat_cache(true, NULL, 0); } RETURN_LONG((zend_long) pid); diff --git a/ext/pcntl/tests/gh23986.phpt b/ext/pcntl/tests/gh23986.phpt new file mode 100644 index 000000000000..7b9976d50079 --- /dev/null +++ b/ext/pcntl/tests/gh23986.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-23986 (/proc/self paths resolve to the parent process after pcntl_fork()) +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) From e6184749b502db932e5dd06a8ad306a9ad7719ed Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Tue, 29 Sep 2026 17:13:13 +0100 Subject: [PATCH 10/23] Reset field_count for OK packet (#23890) --- NEWS | 3 ++ .../mysqli_stmt_next_result_field_count.phpt | 34 ++++++++++++++++ ext/mysqlnd/mysqlnd_ps.c | 1 + ...pdo_mysql_stmt_nextrowset_columncount.phpt | 39 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 ext/mysqli/tests/mysqli_stmt_next_result_field_count.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_stmt_nextrowset_columncount.phpt diff --git a/NEWS b/NEWS index 4ace934375fd..7424a378c896 100644 --- a/NEWS +++ b/NEWS @@ -68,6 +68,9 @@ PHP NEWS . Fix GH-22854: Fixed failed assertion when accessing mysqli property after failed reconnection. (Kamil Tekiela) +- MySQLnd: + . Fixed field_count not resetting on OK packet. (Kamil Tekiela) + - Opcache: . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with FCCs and inlining). (ndossche) diff --git a/ext/mysqli/tests/mysqli_stmt_next_result_field_count.phpt b/ext/mysqli/tests/mysqli_stmt_next_result_field_count.phpt new file mode 100644 index 000000000000..4b04151d054d --- /dev/null +++ b/ext/mysqli/tests/mysqli_stmt_next_result_field_count.phpt @@ -0,0 +1,34 @@ +--TEST-- +mysqli_stmt::$field_count is 0 for the trailing OK packet of a stored procedure +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +query('DROP PROCEDURE IF EXISTS test_field_count_p'); +$link->query('CREATE PROCEDURE test_field_count_p() BEGIN SELECT 1 AS a; SELECT 2 AS b, 3 AS c; END'); + +$stmt = $link->prepare('CALL test_field_count_p()'); +$stmt->execute(); +do { + var_dump($stmt->field_count); + $stmt->get_result(); +} while ($stmt->next_result()); +?> +--CLEAN-- +query('DROP PROCEDURE IF EXISTS test_field_count_p'); +?> +--EXPECT-- +int(1) +int(2) +int(0) diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index 28527c66ccff..681c8f2a4603 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -515,6 +515,7 @@ mysqlnd_stmt_execute_parse_response(MYSQLND_STMT * const s, enum_mysqlnd_parse_e stmt->state = MYSQLND_STMT_EXECUTED; if (conn->last_query_type == QUERY_UPSERT || conn->last_query_type == QUERY_LOAD_LOCAL) { + stmt->field_count = conn->field_count; DBG_INF("PASS"); DBG_RETURN(PASS); } diff --git a/ext/pdo_mysql/tests/pdo_mysql_stmt_nextrowset_columncount.phpt b/ext/pdo_mysql/tests/pdo_mysql_stmt_nextrowset_columncount.phpt new file mode 100644 index 000000000000..94d739dc7c59 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_stmt_nextrowset_columncount.phpt @@ -0,0 +1,39 @@ +--TEST-- +MySQL PDOStatement->columnCount() is 0 for the trailing OK packet of a stored procedure +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- +exec('DROP PROCEDURE IF EXISTS pdo_mysql_stmt_nextrowset_columncount_p'); +$db->exec('CREATE PROCEDURE pdo_mysql_stmt_nextrowset_columncount_p() BEGIN SELECT 1 AS a; SELECT 2 AS b, 3 AS c; END'); + +foreach ([true, false] as $emulate) { + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate); + $stmt = $db->prepare('CALL pdo_mysql_stmt_nextrowset_columncount_p()'); + $stmt->execute(); + do { + var_dump($stmt->columnCount()); + $stmt->fetchAll(); + } while ($stmt->nextRowset()); +} +?> +--CLEAN-- +exec('DROP PROCEDURE IF EXISTS pdo_mysql_stmt_nextrowset_columncount_p'); +?> +--EXPECT-- +int(1) +int(2) +int(0) +int(1) +int(2) +int(0) From 9a3f37872bae87312f29333c4ccd923152dedb33 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Wed, 15 Apr 2026 15:52:44 +0100 Subject: [PATCH 11/23] Fix memory leak when closing a statement on a killed connection Closes GH-21765 --- NEWS | 2 ++ ext/mysqli/mysqli.c | 1 - .../mysqli_stmt_close_killed_connection.phpt | 32 +++++++++++++++++++ ext/mysqlnd/mysqlnd_ps.c | 12 ++----- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt diff --git a/NEWS b/NEWS index 7424a378c896..786f1fb0e123 100644 --- a/NEWS +++ b/NEWS @@ -70,6 +70,8 @@ PHP NEWS - MySQLnd: . Fixed field_count not resetting on OK packet. (Kamil Tekiela) + . Fixed memory leak when closing a prepared statement after its connection + was killed. (Kamil Tekiela) - Opcache: . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c index 987183aa9dcb..5027a8dcd9fe 100644 --- a/ext/mysqli/mysqli.c +++ b/ext/mysqli/mysqli.c @@ -116,7 +116,6 @@ void php_clear_stmt_bind(MY_STMT *stmt) if (stmt->stmt) { if (mysqli_stmt_close(stmt->stmt, true)) { php_error_docref(NULL, E_WARNING, "Error occurred while closing statement"); - return; } } diff --git a/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt b/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt new file mode 100644 index 000000000000..8b573a7666cb --- /dev/null +++ b/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt @@ -0,0 +1,32 @@ +--TEST-- +Closing a prepared statement after its connection was killed must not leak +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +prepare('DO 1'); +} +$link->query('KILL ' . $link->thread_id); + +// Over TCP, a few COM_STMT_CLOSE writes may be accepted before one fails +do { + array_pop($stmts); + usleep(1000); +} while ($stmts && !$link->errno); + +echo "done!\n"; +?> +--EXPECTF-- +Warning: main(): Error occurred while closing statement in %s on line %d +done! diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index 681c8f2a4603..9254c58e58d7 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -1758,6 +1758,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo MYSQLND_STMT_DATA * stmt = s? s->data : NULL; MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL; enum_mysqlnd_collected_stats statistic = STAT_LAST; + enum_func_status ret = PASS; DBG_ENTER("mysqlnd_stmt::close_on_server"); if (!stmt || !conn) { @@ -1795,14 +1796,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo STAT_FREE_RESULT_EXPLICIT); if (GET_CONNECTION_STATE(&conn->state) == CONN_READY) { - enum_func_status ret = FAIL; - const size_t stmt_id = stmt->stmt_id; - - ret = conn->command->stmt_close(conn, stmt_id); - if (ret == FAIL) { - COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info); - DBG_RETURN(FAIL); - } + ret = conn->command->stmt_close(conn, stmt->stmt_id); } } switch (stmt->execute_count) { @@ -1831,7 +1825,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo stmt->conn = NULL; } - DBG_RETURN(PASS); + DBG_RETURN(ret); } /* }}} */ From f9639b562af53493f92d656917e403a17c563e57 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:00:25 +0200 Subject: [PATCH 12/23] Fix __isset escape analysis causing misoptimization Co-authored-by: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> --- Zend/Optimizer/escape_analysis.c | 2 ++ ext/opcache/tests/opt/dce_016.phpt | 50 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 ext/opcache/tests/opt/dce_016.phpt diff --git a/Zend/Optimizer/escape_analysis.c b/Zend/Optimizer/escape_analysis.c index 352d647e925b..0287da7286f2 100644 --- a/Zend/Optimizer/escape_analysis.c +++ b/Zend/Optimizer/escape_analysis.c @@ -173,6 +173,7 @@ static bool is_allocation_def(zend_op_array *op_array, zend_ssa *ssa, int def, i && !ce->destructor && !ce->__get && !ce->__set + && !ce->__isset && !(ce->ce_flags & forbidden_flags) && (ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED)) { return 1; @@ -242,6 +243,7 @@ static bool is_local_def(zend_op_array *op_array, zend_ssa *ssa, int def, int va && !ce->destructor && !ce->__get && !ce->__set + && !ce->__isset && !ce->parent) { return 1; } diff --git a/ext/opcache/tests/opt/dce_016.phpt b/ext/opcache/tests/opt/dce_016.phpt new file mode 100644 index 000000000000..2185a4abfee7 --- /dev/null +++ b/ext/opcache/tests/opt/dce_016.phpt @@ -0,0 +1,50 @@ +--TEST-- +DCE must not remove assignments to properties of an object escaping through __isset +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.file_update_protection=0 +--EXTENSIONS-- +opcache +--FILE-- +foo); + $o->x = 42; +} + +f(); +var_dump($g->x); + +#[AllowDynamicProperties] +class F { + function __isset($n) { + $this->x = 2; + return true; + } +} + +function i() { + $o = new F; + $o->x = 1; + isset($o->foo); + var_dump($o->x); +} +i(); + +?> +--EXPECT-- +int(42) +int(2) From 4bf483fa8814984a7dee7df24ea77f87533e7808 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:02:52 +0200 Subject: [PATCH 13/23] Fix property hook escape analysis causing misoptimization Co-authored-by: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> --- Zend/Optimizer/escape_analysis.c | 2 ++ ext/opcache/tests/opt/dce_017.phpt | 55 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 ext/opcache/tests/opt/dce_017.phpt diff --git a/Zend/Optimizer/escape_analysis.c b/Zend/Optimizer/escape_analysis.c index 0287da7286f2..3c2864d14fb4 100644 --- a/Zend/Optimizer/escape_analysis.c +++ b/Zend/Optimizer/escape_analysis.c @@ -174,6 +174,7 @@ static bool is_allocation_def(zend_op_array *op_array, zend_ssa *ssa, int def, i && !ce->__get && !ce->__set && !ce->__isset + && !ce->num_hooked_props && !(ce->ce_flags & forbidden_flags) && (ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED)) { return 1; @@ -244,6 +245,7 @@ static bool is_local_def(zend_op_array *op_array, zend_ssa *ssa, int def, int va && !ce->__get && !ce->__set && !ce->__isset + && !ce->num_hooked_props && !ce->parent) { return 1; } diff --git a/ext/opcache/tests/opt/dce_017.phpt b/ext/opcache/tests/opt/dce_017.phpt new file mode 100644 index 000000000000..0cf8ad9bf366 --- /dev/null +++ b/ext/opcache/tests/opt/dce_017.phpt @@ -0,0 +1,55 @@ +--TEST-- +DCE must not remove assignments to properties of an object escaping through a property hook +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.file_update_protection=0 +--EXTENSIONS-- +opcache +--FILE-- +h; + $o->x = 42; +} + +g(); +var_dump($g->x); + +class E { + public $x = 0; + public $h { + set { + global $g; + $g = $this; + } + } +} + +function h() { + $o = new E; + $o->h = 1; + $o->x = 42; +} + +h(); +var_dump($g->x); + +?> +--EXPECT-- +int(42) +int(42) From 3b3f5dd12018c67ea748b00b87846724dc9d2411 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:27:17 +0200 Subject: [PATCH 14/23] NEWS --- NEWS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 786f1fb0e123..1bbf5df6062e 100644 --- a/NEWS +++ b/NEWS @@ -84,7 +84,8 @@ PHP NEWS . Fixed bug GH-23637 (PHP-FPM worker SIGSEGV: run_time_cache map_ptr offset beyond CG(map_ptr_last) when a class is reached through the CE cache). (David Carlier) - . Fix incorrect DCE due to unsound escape analysis. (ndossche) + . Fix multiple incorrect DCE due to unsound escape analysis. (ndossche, + arnaud-lb) - OpenSSL: . Fixed stream_socket_enable_crypto() leaving the socket non-blocking From 2dc6626007f9e2ad02eb44cb4c40301a2caed6fd Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 17 Sep 2026 18:50:52 -0400 Subject: [PATCH 15/23] Fix GH-23741: pdo_dblib use-after-free of statement error state pdo_dblib_stmt_execute() published &S->err to the DBPROCESS through dbsetuserdata(), but FreeTDS keeps handing that pointer to the error and message handlers for the life of the connection, long after the statement is freed. A statement cannot retract it from its own destructor, since a GC cycle can free the connection handle first, so every function that hands H->link to libdblib now installs the pdo_dblib_err it owns. get_column_meta() also stops allocating return_value before the dbcoltypeinfo() check that can fail, which the new test would otherwise leak. Fixes GH-23741 Closes GH-23751 --- NEWS | 6 +++++ ext/pdo_dblib/dblib_driver.c | 7 +++++- ext/pdo_dblib/dblib_stmt.c | 14 ++++++++++- ext/pdo_dblib/tests/gh23741.phpt | 38 ++++++++++++++++++++++++++++++ ext/pdo_dblib/tests/gh23741_2.phpt | 27 +++++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_dblib/tests/gh23741.phpt create mode 100644 ext/pdo_dblib/tests/gh23741_2.phpt diff --git a/NEWS b/NEWS index 1bbf5df6062e..a4b3101252e5 100644 --- a/NEWS +++ b/NEWS @@ -117,6 +117,12 @@ PHP NEWS . Fixed PDOStatement::bindParam() and bindColumn() leaking the driver options value. (Ilia Alshanetsky) +- PDO_DBLIB: + . Fixed bug GH-23741 (segfault after a failed query inside a PDO + transaction). Errors raised by beginTransaction(), commit(), rollBack() + and lastInsertId() are now reported instead of being dropped. + (Ilia Alshanetsky) + - PDO_Firebird: . Fixed bug GH-23758 (PDO_Firebird returns null for non-null empty BLOBs). (Lazizbek Ergashev) diff --git a/ext/pdo_dblib/dblib_driver.c b/ext/pdo_dblib/dblib_driver.c index f81e9e7397f3..dd8fb1c320c6 100644 --- a/ext/pdo_dblib/dblib_driver.c +++ b/ext/pdo_dblib/dblib_driver.c @@ -77,11 +77,12 @@ static void dblib_handle_closer(pdo_dbh_t *dbh) pdo_dblib_db_handle *H = (pdo_dblib_db_handle *)dbh->driver_data; if (H) { - pdo_dblib_err_dtor(&H->err); if (H->link) { + dbsetuserdata(H->link, (BYTE*) &H->err); dbclose(H->link); H->link = NULL; } + pdo_dblib_err_dtor(&H->err); if (H->login) { dbfreelogin(H->login); H->login = NULL; @@ -202,6 +203,8 @@ static bool pdo_dblib_transaction_cmd(const char *cmd, pdo_dbh_t *dbh) { pdo_dblib_db_handle *H = (pdo_dblib_db_handle *)dbh->driver_data; + dbsetuserdata(H->link, (BYTE*) &H->err); + if (FAIL == dbcmd(H->link, cmd)) { return false; } @@ -241,6 +244,8 @@ zend_string *dblib_handle_last_id(pdo_dbh_t *dbh, const zend_string *name) * Would use scope_identity() but it's not implemented on Sybase */ + dbsetuserdata(H->link, (BYTE*) &H->err); + if (FAIL == dbcmd(H->link, "SELECT @@IDENTITY")) { return NULL; } diff --git a/ext/pdo_dblib/dblib_stmt.c b/ext/pdo_dblib/dblib_stmt.c index e6e91b60fa27..32dd3d261c51 100644 --- a/ext/pdo_dblib/dblib_stmt.c +++ b/ext/pdo_dblib/dblib_stmt.c @@ -95,6 +95,8 @@ static int pdo_dblib_stmt_cursor_closer(pdo_stmt_t *stmt) pdo_dblib_stmt *S = (pdo_dblib_stmt*)stmt->driver_data; pdo_dblib_db_handle *H = S->H; + dbsetuserdata(H->link, (BYTE*) &S->err); + /* Cancel any pending results */ dbcancel(H->link); @@ -152,6 +154,8 @@ static int pdo_dblib_stmt_next_rowset(pdo_stmt_t *stmt) pdo_dblib_db_handle *H = S->H; RETCODE ret = SUCCESS; + dbsetuserdata(H->link, (BYTE*) &S->err); + /* Ideally use dbcanquery here, but there is a bug in FreeTDS's implementation of dbcanquery * It has been resolved but is currently only available in nightly builds */ @@ -201,6 +205,8 @@ static int pdo_dblib_stmt_fetch(pdo_stmt_t *stmt, pdo_dblib_stmt *S = (pdo_dblib_stmt*)stmt->driver_data; pdo_dblib_db_handle *H = S->H; + dbsetuserdata(H->link, (BYTE*) &S->err); + ret = dbnextrow(H->link); if (FAIL == ret) { @@ -226,6 +232,8 @@ static int pdo_dblib_stmt_describe(pdo_stmt_t *stmt, int colno) return FAILURE; } + dbsetuserdata(H->link, (BYTE*) &S->err); + if (colno == 0) { S->computed_column_name_count = 0; } @@ -350,6 +358,8 @@ static int pdo_dblib_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *zv, enum pd DBCHAR *tmp_data; DBINT data_len, tmp_data_len; + dbsetuserdata(H->link, (BYTE*) &S->err); + coltype = dbcoltype(H->link, colno+1); data = dbdata(H->link, colno+1); data_len = dbdatlen(H->link, colno+1); @@ -472,7 +482,7 @@ static int pdo_dblib_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zva return FAILURE; } - array_init(return_value); + dbsetuserdata(H->link, (BYTE*) &S->err); dbtypeinfo = dbcoltypeinfo(H->link, colno+1); @@ -480,6 +490,8 @@ static int pdo_dblib_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zva coltype = dbcoltype(H->link, colno+1); + array_init(return_value); + add_assoc_long(return_value, "max_length", dbcollen(H->link, colno+1) ); add_assoc_long(return_value, "precision", (int) dbtypeinfo->precision ); add_assoc_long(return_value, "scale", (int) dbtypeinfo->scale ); diff --git a/ext/pdo_dblib/tests/gh23741.phpt b/ext/pdo_dblib/tests/gh23741.phpt new file mode 100644 index 000000000000..f3fc1a5ce445 --- /dev/null +++ b/ext/pdo_dblib/tests/gh23741.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-23741 (pdo_dblib: segfault after a failed query inside a PDO transaction) +--EXTENSIONS-- +pdo_dblib +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_SILENT]); + +$db->query('CREATE TABLE gh23741 (id int)'); + +$db->beginTransaction(); +echo 'failing query inside the transaction: '; +var_dump($db->query('CREATE VIEW gh23741 AS SELECT 1 AS x')); +$db->rollBack(); + +echo 'query after the failure: '; +var_dump($db->query('DROP TABLE IF EXISTS gh23741') instanceof PDOStatement); + +echo "survived connection teardown\n"; +?> +--CLEAN-- +exec('DROP VIEW IF EXISTS gh23741'); +$db->exec('DROP TABLE IF EXISTS gh23741'); +?> +--EXPECT-- +failing query inside the transaction: bool(false) +query after the failure: bool(true) +survived connection teardown diff --git a/ext/pdo_dblib/tests/gh23741_2.phpt b/ext/pdo_dblib/tests/gh23741_2.phpt new file mode 100644 index 000000000000..d8297c3671ea --- /dev/null +++ b/ext/pdo_dblib/tests/gh23741_2.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-23741 (pdo_dblib: crash reading metadata after a sibling statement is freed) +--EXTENSIONS-- +pdo_dblib +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_SILENT]); + +$wide = $db->query('SELECT 1 AS a, 2 AS b, 3 AS c'); +$narrow = $db->query('SELECT 9 AS z'); +unset($narrow); + +echo 'metadata for a column the connection no longer has: '; +var_dump($wide->getColumnMeta(2)); + +echo "survived the out-of-range column\n"; +?> +--EXPECT-- +metadata for a column the connection no longer has: bool(false) +survived the out-of-range column From efb8ad276d6fbd1d8b667607cf71248f55a0d5d1 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 26 Sep 2026 18:15:03 -0400 Subject: [PATCH 16/23] ext/libxml: Keep SimpleXML children alive across reconstruction Reconstructing a SimpleXMLElement decremented the shared node and then installed a new document while the old pointer was still set, so the second decrement freed a node a child object still held. Clear the pointer before the new node is installed. Closes GH-23931 --- NEWS | 4 ++++ ext/libxml/libxml.c | 1 + .../reconstruct_with_retained_child.phpt | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 ext/simplexml/tests/reconstruct_with_retained_child.phpt diff --git a/NEWS b/NEWS index a4b3101252e5..e5441fe6bc26 100644 --- a/NEWS +++ b/NEWS @@ -137,6 +137,10 @@ PHP NEWS . Fixed exceptions from user-defined create_sid() handlers being replaced by return-value validation errors. (Ilia Alshanetsky) +- SimpleXML: + . Fixed reconstructing a SimpleXMLElement freeing a child element that + another variable still references. (Ilia Alshanetsky) + - Sockets: . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on Windows. (David Carlier) diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index c73bcf930cfd..5fdc44c4115f 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -1501,6 +1501,7 @@ PHP_LIBXML_API void php_libxml_node_decrement_resource(php_libxml_node_object *o obj_node->_private = NULL; } } + object->node = NULL; } if (object != NULL && object->document != NULL) { /* Safe to call as if the resource were freed then doc pointer is NULL */ diff --git a/ext/simplexml/tests/reconstruct_with_retained_child.phpt b/ext/simplexml/tests/reconstruct_with_retained_child.phpt new file mode 100644 index 000000000000..c6a1cb418089 --- /dev/null +++ b/ext/simplexml/tests/reconstruct_with_retained_child.phpt @@ -0,0 +1,19 @@ +--TEST-- +SimpleXMLElement reconstruction with a retained child +--EXTENSIONS-- +simplexml +--FILE-- +old'); +$child = $xml->child; + +$xml->__construct('new'); + +var_dump((string) $xml->new); +var_dump((string) $child); +var_dump($child->asXML()); +?> +--EXPECT-- +string(3) "new" +string(3) "old" +string(18) "old" From 77bdd80927d9440d17be7dce7c06a2cc2292b40a Mon Sep 17 00:00:00 2001 From: Alexander Danilov Date: Tue, 29 Sep 2026 20:54:39 +0300 Subject: [PATCH 17/23] openssl: Fix memory leak by doing early salt validation Closes GH-23999. --- NEWS | 1 + ext/openssl/openssl.c | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 18d07cc32fff..478e2291210e 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,7 @@ PHP NEWS - OpenSSL: . Fixed stream_socket_enable_crypto() leaving the socket non-blocking after a handshake timeout. (Ilia Alshanetsky) + . Fix memory leak by doing early salt validation. (adapik) - PCNTL: . Fixed pcntl_signal_dispatch() dropping the queued signals when it runs while diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 5ea2a7737434..6ec8a11734a9 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -4556,6 +4556,8 @@ PHP_FUNCTION(openssl_sign) Z_PARAM_LONG(salt_length) ZEND_PARSE_PARAMETERS_END(); + PHP_OPENSSL_CHECK_LONG_TO_INT(salt_length, salt_length, 6); + pkey = php_openssl_pkey_from_zval(key, 0, "", 0, 3); if (pkey == NULL) { if (!EG(exception)) { @@ -4574,7 +4576,6 @@ PHP_FUNCTION(openssl_sign) php_error_docref(NULL, E_WARNING, "Unknown digest algorithm"); RETURN_FALSE; } - PHP_OPENSSL_CHECK_LONG_TO_INT(salt_length, salt_length, 6); md_ctx = EVP_MD_CTX_create(); size_t siglen; From 0c6db8ccfdc207398e9724c36824abae1d6ca4f2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 29 Sep 2026 21:17:39 +0200 Subject: [PATCH 18/23] Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (#23825) HAVE_EPOLL_PWAIT2 only tells whether the libc exports the wrapper, which glibc does since 2.35 regardless of the running kernel. A PHP built on a kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from every Context::wait() call, which makes the epoll backend and thus the Auto backend unusable. The same happens under emulation layers that do not implement the syscall. Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to epoll_wait with a millisecond timeout, retrying the current call so the failure is never visible to the caller. The flag is process wide since kernel support is the same for every thread, and it is atomic so the first concurrent waits in a ZTS build do not race on it. --- main/poll/poll_backend_epoll.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 1cc05b3005e5..85660a9322fe 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -18,6 +18,11 @@ #include +#ifdef HAVE_EPOLL_PWAIT2 +/* Cleared when the running kernel returns ENOSYS */ +static zend_atomic_bool epoll_pwait2_available = ZEND_ATOMIC_BOOL_INITIALIZER(true); +#endif + typedef struct epoll_backend_data { int epoll_fd; struct epoll_event *events; @@ -186,13 +191,21 @@ static int epoll_backend_wait( backend_data->events_capacity = max_events; } - int nfds; + int nfds = 0; #ifdef HAVE_EPOLL_PWAIT2 - nfds = epoll_pwait2(backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL); -#else - int timeout_ms = php_poll_timespec_to_ms(timeout); - nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout_ms); + if (EXPECTED(zend_atomic_bool_load_ex(&epoll_pwait2_available))) { + nfds = epoll_pwait2( + backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL); + if (UNEXPECTED(nfds < 0 && (errno == ENOSYS || errno == ENOTSUP))) { + zend_atomic_bool_store_ex(&epoll_pwait2_available, false); + } + } + if (UNEXPECTED(!zend_atomic_bool_load_ex(&epoll_pwait2_available))) #endif + { + int timeout_ms = php_poll_timespec_to_ms(timeout); + nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout_ms); + } if (nfds > 0) { for (int i = 0; i < nfds; i++) { From 5261965b8f09cd37a76298a9a419584852ee34d8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 29 Sep 2026 21:17:39 +0200 Subject: [PATCH 19/23] Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (#23825) HAVE_EPOLL_PWAIT2 only tells whether the libc exports the wrapper, which glibc does since 2.35 regardless of the running kernel. A PHP built on a kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from every Context::wait() call, which makes the epoll backend and thus the Auto backend unusable. The same happens under emulation layers that do not implement the syscall. Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to epoll_wait with a millisecond timeout, retrying the current call so the failure is never visible to the caller. The flag is process wide since kernel support is the same for every thread, and it is atomic so the first concurrent waits in a ZTS build do not race on it. --- main/poll/poll_backend_epoll.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 1cc05b3005e5..85660a9322fe 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -18,6 +18,11 @@ #include +#ifdef HAVE_EPOLL_PWAIT2 +/* Cleared when the running kernel returns ENOSYS */ +static zend_atomic_bool epoll_pwait2_available = ZEND_ATOMIC_BOOL_INITIALIZER(true); +#endif + typedef struct epoll_backend_data { int epoll_fd; struct epoll_event *events; @@ -186,13 +191,21 @@ static int epoll_backend_wait( backend_data->events_capacity = max_events; } - int nfds; + int nfds = 0; #ifdef HAVE_EPOLL_PWAIT2 - nfds = epoll_pwait2(backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL); -#else - int timeout_ms = php_poll_timespec_to_ms(timeout); - nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout_ms); + if (EXPECTED(zend_atomic_bool_load_ex(&epoll_pwait2_available))) { + nfds = epoll_pwait2( + backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL); + if (UNEXPECTED(nfds < 0 && (errno == ENOSYS || errno == ENOTSUP))) { + zend_atomic_bool_store_ex(&epoll_pwait2_available, false); + } + } + if (UNEXPECTED(!zend_atomic_bool_load_ex(&epoll_pwait2_available))) #endif + { + int timeout_ms = php_poll_timespec_to_ms(timeout); + nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout_ms); + } if (nfds > 0) { for (int i = 0; i < nfds; i++) { From e0eea65aa447805c677a9fb0a33bbc65cd79eb68 Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 22 Sep 2026 16:21:45 +0500 Subject: [PATCH 20/23] Fix GH-23842: skipLazyInitialization() copies unresolved constant defaults Closes GH-23843. --- NEWS | 2 ++ Zend/tests/lazy_objects/gh23842.phpt | 32 ++++++++++++++++++++++++++++ ext/reflection/php_reflection.c | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/lazy_objects/gh23842.phpt diff --git a/NEWS b/NEWS index e5441fe6bc26..0ec3c10f0a12 100644 --- a/NEWS +++ b/NEWS @@ -404,6 +404,8 @@ PHP NEWS parent slot when a child class hooks an inherited property. (iliaal) . Fixed segfault in ReflectionMethod::createFromMethodName() on an uninstantiable subclass. (iliaal) + . Fixed bug GH-23842 (ReflectionProperty::skipLazyInitialization() copies + invalid constant defaults with OPcache). (DirkTrunkstar, Lazizbek Ergashev) - Readline: . Fixed class constant completion in the interactive shell. (Weilin Du) diff --git a/Zend/tests/lazy_objects/gh23842.phpt b/Zend/tests/lazy_objects/gh23842.phpt new file mode 100644 index 000000000000..fe791dde685b --- /dev/null +++ b/Zend/tests/lazy_objects/gh23842.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-23842: skipLazyInitialization() copies an unresolved constant default with opcache +--CREDITS-- +DirkTrunkstar +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +--FILE-- +newLazyGhost(function () { + throw new \Exception('initializer'); +}); +$reflector->getProperty('currency')->skipLazyInitialization($product); + +var_dump($product->currency); + +?> +--EXPECT-- +string(3) "EUR" diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index c7b15c63a0c5..2c94a6e80d18 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6461,7 +6461,7 @@ ZEND_METHOD(ReflectionProperty, skipLazyInitialization) RETURN_THROWS(); } - zval *src = &object->ce->default_properties_table[OBJ_PROP_TO_NUM(prop->offset)]; + zval *src = &CE_DEFAULT_PROPERTIES_TABLE(object->ce)[OBJ_PROP_TO_NUM(prop->offset)]; zval *dst = OBJ_PROP(object, prop->offset); if (!(Z_PROP_FLAG_P(dst) & IS_PROP_LAZY)) { From e7b225697ef2251e3ecfdd21984489c633f94101 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:08:15 +0200 Subject: [PATCH 21/23] Fix GH-23980: ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL (include/eval run with a pending exception) Closes GH-24002. --- NEWS | 1 + Zend/zend_vm_def.h | 6 +++++- Zend/zend_vm_execute.h | 24 ++++++++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 0ec3c10f0a12..dbb54c4cbaa9 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,7 @@ PHP NEWS (ndossche) . Fixed AVX being reported as supported when the OS has not enabled AVX state. (Ilia Alshanetsky) + . Fixed GH-23980 (ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL). (ndossche) - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index c46b17101546..566c9b4c4563 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6585,7 +6585,11 @@ ZEND_VM_HANDLER(73, ZEND_INCLUDE_OR_EVAL, CONST|TMPVAR|CV, ANY, EVAL, SPEC(OBSER } } FREE_OP1(); - ZEND_VM_NEXT_OPCODE(); + if (OP1_TYPE & IS_CONST) { + ZEND_VM_NEXT_OPCODE(); + } else { + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); + } } ZEND_VM_HANDLER(153, ZEND_UNSET_CV, CV, UNUSED) diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index f4f35ce2a8a9..2ebbfaa9c05d 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5306,7 +5306,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HAN } } - ZEND_VM_NEXT_OPCODE(); + if (IS_CONST & IS_CONST) { + ZEND_VM_NEXT_OPCODE(); + } else { + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); + } } static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_OBSERVER_HANDLER(ZEND_OPCODE_HANDLER_ARGS) @@ -5389,7 +5393,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_OBSERVER_ } } FREE_OP(opline->op1_type, opline->op1.var); - ZEND_VM_NEXT_OPCODE(); + if (opline->op1_type & IS_CONST) { + ZEND_VM_NEXT_OPCODE(); + } else { + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); + } } static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) @@ -15394,7 +15402,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_TMPVAR_HA } } zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); - ZEND_VM_NEXT_OPCODE(); + if ((IS_TMP_VAR|IS_VAR) & IS_CONST) { + ZEND_VM_NEXT_OPCODE(); + } else { + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); + } } static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_YIELD_FROM_SPEC_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS) @@ -41213,7 +41225,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CV_HANDLE } } - ZEND_VM_NEXT_OPCODE(); + if (IS_CV & IS_CONST) { + ZEND_VM_NEXT_OPCODE(); + } else { + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); + } } static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) From a85fcf30ab6c66464e3e7f4b8ca500b2fc9221f2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 29 Sep 2026 21:44:27 +0200 Subject: [PATCH 22/23] Use C11 atomics for epoll_pwait2_available --- main/poll/poll_backend_epoll.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 85660a9322fe..ef8d42435ec6 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -20,7 +20,7 @@ #ifdef HAVE_EPOLL_PWAIT2 /* Cleared when the running kernel returns ENOSYS */ -static zend_atomic_bool epoll_pwait2_available = ZEND_ATOMIC_BOOL_INITIALIZER(true); +static atomic_bool epoll_pwait2_available = true; #endif typedef struct epoll_backend_data { @@ -193,14 +193,14 @@ static int epoll_backend_wait( int nfds = 0; #ifdef HAVE_EPOLL_PWAIT2 - if (EXPECTED(zend_atomic_bool_load_ex(&epoll_pwait2_available))) { + if (EXPECTED(atomic_load(&epoll_pwait2_available))) { nfds = epoll_pwait2( backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL); if (UNEXPECTED(nfds < 0 && (errno == ENOSYS || errno == ENOTSUP))) { - zend_atomic_bool_store_ex(&epoll_pwait2_available, false); + atomic_store(&epoll_pwait2_available, false); } } - if (UNEXPECTED(!zend_atomic_bool_load_ex(&epoll_pwait2_available))) + if (UNEXPECTED(!atomic_load(&epoll_pwait2_available))) #endif { int timeout_ms = php_poll_timespec_to_ms(timeout); From 8184e9efec27e6c76698e7c67a0e108793402912 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 25 Sep 2026 04:55:07 -0400 Subject: [PATCH 23/23] ext/bcmath: Clear the sign of BcMath\Number results that truncate to zero A bc_num that is zero must carry PLUS, since bc_compare() orders by sign first. The bc_divide() fast paths for a divisor of +/-1 or a power of ten, bc_raise() with a positive exponent, and the Number add/sub/mul helpers truncate to the requested scale and keep the operand sign, so (new BcMath\Number('-0.001'))->div(1, 0) compares less than 0, is not equal to 0, and makes sqrt() throw. Closes GH-23967 --- NEWS | 4 +++ ext/bcmath/bcmath.c | 9 +++++ ext/bcmath/libbcmath/src/div.c | 12 +++++-- ext/bcmath/libbcmath/src/raise.c | 3 ++ .../calc_methods_zero_result_sign.phpt | 35 +++++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 ext/bcmath/tests/number/methods/calc_methods_zero_result_sign.phpt diff --git a/NEWS b/NEWS index dbb54c4cbaa9..a011c225aa69 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.27 +- BCMath: + . Fixed BcMath\Number results that truncate to zero keeping a negative sign + and comparing less than zero. (Ilia Alshanetsky) + - CLI . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during request activation). (matyhtf) diff --git a/ext/bcmath/bcmath.c b/ext/bcmath/bcmath.c index e54e5bfac774..4bdd8a234237 100644 --- a/ext/bcmath/bcmath.c +++ b/ext/bcmath/bcmath.c @@ -1048,6 +1048,9 @@ static zend_always_inline void bcmath_number_add_internal( } *ret = bc_add(n1, n2, *scale); (*ret)->n_scale = MIN(*scale, (*ret)->n_scale); + if (bc_is_zero(*ret)) { + (*ret)->n_sign = PLUS; + } bc_rm_trailing_zeros(*ret); } @@ -1060,6 +1063,9 @@ static zend_always_inline void bcmath_number_sub_internal( } *ret = bc_sub(n1, n2, *scale); (*ret)->n_scale = MIN(*scale, (*ret)->n_scale); + if (bc_is_zero(*ret)) { + (*ret)->n_sign = PLUS; + } bc_rm_trailing_zeros(*ret); } @@ -1076,6 +1082,9 @@ static zend_always_inline zend_result bcmath_number_mul_internal( } *ret = bc_multiply(n1, n2, *scale); (*ret)->n_scale = MIN(*scale, (*ret)->n_scale); + if (bc_is_zero(*ret)) { + (*ret)->n_sign = PLUS; + } bc_rm_trailing_zeros(*ret); return SUCCESS; } diff --git a/ext/bcmath/libbcmath/src/div.c b/ext/bcmath/libbcmath/src/div.c index ce9ae1e1dd79..a45ffdb77575 100644 --- a/ext/bcmath/libbcmath/src/div.c +++ b/ext/bcmath/libbcmath/src/div.c @@ -349,8 +349,12 @@ bool bc_divide(bc_num numerator, bc_num divisor, bc_num *quot, size_t scale) *quot = bc_new_num_nonzeroed(numerator->n_len, quot_scale); char *qptr = (*quot)->n_value; memcpy(qptr, numerator->n_value, numerator->n_len + quot_scale); - (*quot)->n_sign = numerator->n_sign == divisor->n_sign ? PLUS : MINUS; _bc_rm_leading_zeros(*quot); + if (bc_is_zero(*quot)) { + (*quot)->n_sign = PLUS; + } else { + (*quot)->n_sign = numerator->n_sign == divisor->n_sign ? PLUS : MINUS; + } return true; } @@ -475,7 +479,11 @@ bool bc_divide(bc_num numerator, bc_num divisor, bc_num *quot, size_t scale) for (size_t i = 0; i < numerator_bottom_extension; i++) { *qptr++ = 0; } - (*quot)->n_sign = numerator->n_sign == divisor->n_sign ? PLUS : MINUS; + if (bc_is_zero(*quot)) { + (*quot)->n_sign = PLUS; + } else { + (*quot)->n_sign = numerator->n_sign == divisor->n_sign ? PLUS : MINUS; + } return true; } diff --git a/ext/bcmath/libbcmath/src/raise.c b/ext/bcmath/libbcmath/src/raise.c index 1e283864694b..efb30f24ffce 100644 --- a/ext/bcmath/libbcmath/src/raise.c +++ b/ext/bcmath/libbcmath/src/raise.c @@ -102,6 +102,9 @@ bool bc_raise(bc_num base, long exponent, bc_num *result, size_t scale) { bc_free_num (result); *result = temp; (*result)->n_scale = MIN(scale, (*result)->n_scale); + if (bc_is_zero(*result)) { + (*result)->n_sign = PLUS; + } } bc_free_num (&power); return true; diff --git a/ext/bcmath/tests/number/methods/calc_methods_zero_result_sign.phpt b/ext/bcmath/tests/number/methods/calc_methods_zero_result_sign.phpt new file mode 100644 index 000000000000..1871f0e3ca2c --- /dev/null +++ b/ext/bcmath/tests/number/methods/calc_methods_zero_result_sign.phpt @@ -0,0 +1,35 @@ +--TEST-- +BcMath\Number calc methods return an unsigned zero when the result truncates to zero +--EXTENSIONS-- +bcmath +--FILE-- +$method($arg, $scale); + echo "{$num} {$method} {$arg}: {$ret} ", $ret <=> 0, ' ', var_export($ret == 0, true), "\n"; +} + +[$quot, $rem] = (new BcMath\Number('-0.001'))->divmod('1', 0); +echo "-0.001 divmod 1: {$quot} ", $quot <=> 0, ' ', var_export($quot == 0, true), "\n"; +?> +--EXPECT-- +-0.001 div 1: 0 0 true +0.001 div -1: 0 0 true +-0.001 div 10: 0.00 0 true +-0.001 div 0.1: 0.0 0 true +-0.001 add -0.001: 0 0 true +-0.001 sub 0.001: 0 0 true +-0.001 mul 1: 0 0 true +-0.1 pow 3: 0.00 0 true +-0.001 divmod 1: 0 0 true