From a1cb5a37c0f048128ebe9784fda8fc279a317e61 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 24 Sep 2026 15:33:28 +0200 Subject: [PATCH 1/7] fix aarch64 gcc preserve_none detection (#23883) --- Zend/Zend.m4 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Zend/Zend.m4 b/Zend/Zend.m4 index 6353ea2ec7a8..1f4e5e26f00e 100644 --- a/Zend/Zend.m4 +++ b/Zend/Zend.m4 @@ -513,7 +513,7 @@ uintptr_t __attribute__((preserve_none,noinline,used)) fun(uintptr_t a, uintptr_ return (uintptr_t)const3; } -uintptr_t __attribute__((preserve_none)) test(void) { +uintptr_t __attribute__((preserve_none,noinline)) test(void) { uintptr_t ret; #if defined(__x86_64__) @@ -531,7 +531,7 @@ uintptr_t __attribute__((preserve_none)) test(void) { #endif : "=a" (ret) : "r" (const1), "r" (const2), "r" (key) - : "r12", "r13" + : "r12", "r13", "memory", "cc" ); #elif defined(__aarch64__) __asm__ __volatile__( @@ -547,7 +547,7 @@ uintptr_t __attribute__((preserve_none)) test(void) { "mov %0, x0\n" : "=r" (ret) : "r" (const1), "r" (const2), "r" (key) - : "x0", "x21", "x22", "x30" + : "x0", "x20", "x21", "x30", "memory", "cc" ); #else # error From dc7962c43e417579c276f4fedfab4c3d53f45799 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 24 Sep 2026 17:26:05 +0200 Subject: [PATCH 2/7] perf: use nl_langinfo for the decimal point on glibc ZTS instead of mutex-protected localeconv (#22728) --- ext/standard/formatted_print.c | 16 +++++----------- ext/standard/php_string.h | 3 +++ ext/standard/string.c | 17 +++++++++++++++++ main/snprintf.c | 14 ++++---------- main/spprintf.c | 14 ++++---------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/ext/standard/formatted_print.c b/ext/standard/formatted_print.c index f5ac2ce58456..aa6723f8698a 100644 --- a/ext/standard/formatted_print.c +++ b/ext/standard/formatted_print.c @@ -18,8 +18,8 @@ #include #ifdef ZTS -#include "ext/standard/php_string.h" /* for localeconv_r() */ -#define LCONV_DECIMAL_POINT (*lconv.decimal_point) +#include "ext/standard/php_string.h" /* for localeconv_decimal_point() */ +#define LCONV_DECIMAL_POINT localeconv_decimal_point() #else #define LCONV_DECIMAL_POINT (*lconv->decimal_point) #endif @@ -221,9 +221,7 @@ php_sprintf_appenddouble(zend_string **buffer, size_t *pos, char *s = NULL; size_t s_len = 0; bool is_negative = false; -#ifdef ZTS - struct lconv lconv; -#else +#ifndef ZTS struct lconv *lconv; #endif @@ -256,9 +254,7 @@ php_sprintf_appenddouble(zend_string **buffer, size_t *pos, case 'E': case 'f': case 'F': -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS lconv = localeconv(); #endif s = php_conv_fp((fmt == 'f')?'F':fmt, number, 0, precision, @@ -285,9 +281,7 @@ php_sprintf_appenddouble(zend_string **buffer, size_t *pos, char decimal_point = '.'; if (fmt == 'g' || fmt == 'G') { -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS lconv = localeconv(); #endif decimal_point = LCONV_DECIMAL_POINT; diff --git a/ext/standard/php_string.h b/ext/standard/php_string.h index 86c331f8a7c5..412a4a4fa7a6 100644 --- a/ext/standard/php_string.h +++ b/ext/standard/php_string.h @@ -32,6 +32,9 @@ PHP_MINIT_FUNCTION(string_intrin); strnatcmp_ex(a, strlen(a), b, strlen(b), true) PHPAPI int strnatcmp_ex(char const *a, size_t a_len, char const *b, size_t b_len, bool is_case_insensitive); PHPAPI struct lconv *localeconv_r(struct lconv *out); +#ifdef ZTS +PHPAPI char localeconv_decimal_point(void); +#endif PHPAPI char *php_strtr(char *str, size_t len, const char *str_from, const char *str_to, size_t trlen); PHPAPI zend_string *php_addslashes(zend_string *str); PHPAPI void php_stripslashes(zend_string *str); diff --git a/ext/standard/string.c b/ext/standard/string.c index 441890ea2902..bfe0c71795bf 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -19,6 +19,9 @@ #include "php_string.h" #include "php_variables.h" #include +#ifdef HAVE_NL_LANGINFO +# include +#endif #ifdef HAVE_LANGINFO_H # include #endif @@ -88,6 +91,20 @@ static zend_string *php_hex2bin(const unsigned char *old, const size_t oldlen) } /* }}} */ +#ifdef ZTS +/* read the decimal point through nl_langinfo() (thread-safe), instead of taking the lock. */ +PHPAPI char localeconv_decimal_point(void) +{ +#if defined(HAVE_NL_LANGINFO) && (defined(__GLIBC__) || defined(__MUSL__)) + return *nl_langinfo(RADIXCHAR); +#else + struct lconv lc; + localeconv_r(&lc); + return *lc.decimal_point; +#endif +} +#endif + /* {{{ localeconv_r * glibc's localeconv is not reentrant, so lets make it so ... sorta */ PHPAPI struct lconv *localeconv_r(struct lconv *out) diff --git a/main/snprintf.c b/main/snprintf.c index 73c981a1cee7..9e9655fb70ad 100644 --- a/main/snprintf.c +++ b/main/snprintf.c @@ -30,7 +30,7 @@ #include #ifdef ZTS #include "ext/standard/php_string.h" -#define LCONV_DECIMAL_POINT (*lconv.decimal_point) +#define LCONV_DECIMAL_POINT localeconv_decimal_point() #else #define LCONV_DECIMAL_POINT (*lconv->decimal_point) #endif @@ -491,9 +491,7 @@ static size_t format_converter(buffy * odp, const char *fmt, va_list ap) /* {{{ char num_buf[NUM_BUF_SIZE]; char char_buf[2]; /* for printing %% and % */ -#ifdef ZTS - struct lconv lconv; -#else +#ifndef ZTS struct lconv *lconv = NULL; #endif @@ -843,9 +841,7 @@ static size_t format_converter(buffy * odp, const char *fmt, va_list ap) /* {{{ s = "INF"; s_len = 3; } else { -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS if (!lconv) { lconv = localeconv(); } @@ -902,9 +898,7 @@ static size_t format_converter(buffy * odp, const char *fmt, va_list ap) /* {{{ /* * * We use &num_buf[ 1 ], so that we have room for the sign */ -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS if (!lconv) { lconv = localeconv(); } diff --git a/main/spprintf.c b/main/spprintf.c index 8d6b80258a72..1975a002be7d 100644 --- a/main/spprintf.c +++ b/main/spprintf.c @@ -88,7 +88,7 @@ #include #ifdef ZTS #include "ext/standard/php_string.h" -#define LCONV_DECIMAL_POINT (*lconv.decimal_point) +#define LCONV_DECIMAL_POINT localeconv_decimal_point() #else #define LCONV_DECIMAL_POINT (*lconv->decimal_point) #endif @@ -196,9 +196,7 @@ static void xbuf_format_converter(void *xbuf, bool is_char, const char *fmt, va_ char num_buf[NUM_BUF_SIZE]; char char_buf[2]; /* for printing %% and % */ -#ifdef ZTS - struct lconv lconv; -#else +#ifndef ZTS struct lconv *lconv = NULL; #endif @@ -557,9 +555,7 @@ format_zend_string:; s = "inf"; s_len = 3; } else { -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS if (!lconv) { lconv = localeconv(); } @@ -615,9 +611,7 @@ format_zend_string:; /* * * We use &num_buf[ 1 ], so that we have room for the sign */ -#ifdef ZTS - localeconv_r(&lconv); -#else +#ifndef ZTS if (!lconv) { lconv = localeconv(); } From 5dfd11b1b0de6d066868ea6384d42d1aacc3b93d Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Fri, 25 Sep 2026 00:23:44 +0800 Subject: [PATCH 3/7] ext/intl: Stop enumCharNames() when the callback throws (#23866) --- .../enumCharNames_callback_exception.phpt | 20 +++++++++++++++++++ ext/intl/uchar/uchar.cpp | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 ext/intl/uchar/tests/enumCharNames_callback_exception.phpt diff --git a/ext/intl/uchar/tests/enumCharNames_callback_exception.phpt b/ext/intl/uchar/tests/enumCharNames_callback_exception.phpt new file mode 100644 index 000000000000..2ebac7aac92c --- /dev/null +++ b/ext/intl/uchar/tests/enumCharNames_callback_exception.phpt @@ -0,0 +1,20 @@ +--TEST-- +IntlChar::enumCharNames() propagates callback exceptions and remains usable +--EXTENSIONS-- +intl +--FILE-- +getMessage(), "\n"; +} +var_dump(IntlChar::enumCharNames(65, 68, static fn() => false)); +?> +--EXPECT-- +65 +Stop enumeration +bool(true) diff --git a/ext/intl/uchar/uchar.cpp b/ext/intl/uchar/uchar.cpp index 1319ec679085..b2df5b9a05a8 100644 --- a/ext/intl/uchar/uchar.cpp +++ b/ext/intl/uchar/uchar.cpp @@ -312,7 +312,7 @@ static UBool enumCharNames_callback(enumCharNames_data *context, } zval_ptr_dtor(&retval); zval_ptr_dtor_str(&args[2]); - return 1; + return !EG(exception); } IC_METHOD(enumCharNames) { UChar32 start, limit; From c4efd2093177d45012412eecc8a97d6759ccbd29 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 20 Sep 2026 00:37:35 +0200 Subject: [PATCH 4/7] Fix OSS-Fuzz #536440507: Immutable class incorrect assertion For immutable classes, the flag for updated constants lives on the mutable part (see zend_update_class_constants). That means the assertion is bogus and can be replaced with a more complex check via a helper function. For the case where we perform the flags check, but not as an assertion but as a proper check, deferring to zend_update_class_constants() is enough because it already checks the flags correctly itself. Closes GH-23781. --- NEWS | 2 ++ .../lazy_objects/oss_fuzz_536440507.phpt | 21 ++++++++++++++ Zend/zend_lazy_objects.c | 28 ++++++++++++++----- 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 Zend/tests/lazy_objects/oss_fuzz_536440507.phpt diff --git a/NEWS b/NEWS index f61ceb97aa00..da8362ce1edb 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,8 @@ PHP NEWS - Core . Fix GH-21999: GC inconsistency with lazy object, var_dump(), and object comparison. (Arnaud) + . Fixed OSS-Fuzz #536440507 (Immutable class incorrect assertion). + (ndossche) - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function diff --git a/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt b/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt new file mode 100644 index 000000000000..02a1fcc69342 --- /dev/null +++ b/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt @@ -0,0 +1,21 @@ +--TEST-- +OSS-Fuzz #536440507 (Immutable class incorrect assertion) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- +newLazyGhost(function ($obj) {}); +var_dump($o->b); + +?> +--EXPECTF-- +Deprecated: Implicit conversion from float 3.4028236692093845E+32 to int loses precision in %s on line %d +int(%s) diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index 2ca3a5e46569..f7ef547bc9cb 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -179,6 +179,18 @@ bool zend_lazy_object_decr_lazy_props(zend_object *obj) return info->lazy_properties_count == 0; } +/* See zend_update_class_constants(). */ +static zend_always_inline bool zend_class_constants_are_updated(const zend_class_entry *ce) { + if (ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED) { + return true; + } + if (ZEND_MAP_PTR(ce->mutable_data)) { + const zend_class_mutable_data *mutable_data = ZEND_MAP_PTR_GET_IMM(ce->mutable_data); + return mutable_data && (mutable_data->ce_flags & ZEND_ACC_CONSTANTS_UPDATED); + } + return false; +} + /** * Making objects lazy */ @@ -259,11 +271,9 @@ ZEND_API zend_object *zend_object_make_lazy(zend_object *obj, return NULL; } - if (UNEXPECTED(!(reflection_ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) { - if (UNEXPECTED(zend_update_class_constants(reflection_ce) != SUCCESS)) { - ZEND_ASSERT(EG(exception)); - return NULL; - } + if (UNEXPECTED(zend_update_class_constants(reflection_ce) != SUCCESS)) { + ZEND_ASSERT(EG(exception)); + return NULL; } obj = zend_objects_new(reflection_ce); @@ -383,7 +393,9 @@ ZEND_API zend_object *zend_lazy_object_mark_as_initialized(zend_object *obj) zend_class_entry *ce = obj->ce; - ZEND_ASSERT(ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED); +#if ZEND_DEBUG + ZEND_ASSERT(zend_class_constants_are_updated(ce)); +#endif zval *default_properties_table = CE_DEFAULT_PROPERTIES_TABLE(ce); zval *properties_table = obj->properties_table; @@ -579,7 +591,9 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj) zend_class_entry *ce = obj->ce; - ZEND_ASSERT(ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED); +#if ZEND_DEBUG + ZEND_ASSERT(zend_class_constants_are_updated(ce)); +#endif if (zend_object_is_lazy_proxy(obj)) { return zend_lazy_object_init_proxy(obj); From 35655073145bb7130c3908fcb25c2551e73bdf1d Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko <640114+lisachenko@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:58:46 +0300 Subject: [PATCH 5/7] Fix GH-23628: Tracing JIT reads undefined property slots of lazy proxies and unset properties (#23640) A lazy proxy keeps its own property slots IS_UNDEF|IS_PROP_LAZY even after it has been initialized, and the object handlers forward every property access to the real instance. The tracing JIT was not aware of this in two places: 1. When the recorded trace contained a FETCH_OBJ_R/IS/W on a known property whose slot was IS_UNDEF, the known-offset fast path was still compiled. For a lazy proxy this path never succeeds, and it deoptimized on every execution. Use the generic code path (that falls back to the object handlers for undefined slots) when the slot was IS_UNDEF at recording time. This also covers uninitialized and unset properties. 2. During deoptimization of a failed result type guard after FETCH_OBJ_IS, an IS_UNDEF slot was turned into NULL, assuming an undefined property. For a slot flagged IS_PROP_LAZY the fetch has to be forwarded to the real instance instead, so re-execute the opline in the VM, the same way it is already done for FETCH_OBJ_R. --- NEWS | 4 ++ ext/opcache/jit/zend_jit_ir.c | 5 ++ ext/opcache/jit/zend_jit_trace.c | 7 ++- ext/opcache/tests/jit/gh23628_001.phpt | 72 ++++++++++++++++++++++++++ ext/opcache/tests/jit/gh23628_002.phpt | 46 ++++++++++++++++ ext/opcache/tests/jit/gh23628_003.phpt | 38 ++++++++++++++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 ext/opcache/tests/jit/gh23628_001.phpt create mode 100644 ext/opcache/tests/jit/gh23628_002.phpt create mode 100644 ext/opcache/tests/jit/gh23628_003.phpt diff --git a/NEWS b/NEWS index 88c009de5c23..f0e89aa088bb 100644 --- a/NEWS +++ b/NEWS @@ -95,6 +95,10 @@ PHP NEWS . Fixed inflate_init() dropping the preset dictionary for raw streams with a non-default window. (Ilia Alshanetsky) +- Opcache: + . Fixed bug GH-23628 (Tracing JIT reads undefined property slots of lazy + proxy objects instead of forwarding to the real instance). (lisachenko) + 24 Sep 2026, PHP 8.5.11 diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 5e4af150861e..660378e7275d 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -14247,6 +14247,11 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, ZEND_ASSERT(Z_TYPE_P(member) == IS_STRING && Z_STRVAL_P(member)[0] != '\0'); prop_info = zend_get_known_property_info(op_array, ce, Z_STR_P(member), on_this, op_array->filename); + if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE && prop_type == IS_UNDEF) { + prop_info = NULL; + trace_ce = NULL; + } + if (on_this) { zend_jit_addr this_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FP, offsetof(zend_execute_data, This)); obj_ref = jit_Z_PTR(jit, this_addr); diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 655c84e3f496..5bb846b518f8 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -8713,10 +8713,13 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf const zend_op *op = t->exit_info[exit_num].opline; ZEND_ASSERT(op); op--; - if (op->opcode == ZEND_FETCH_DIM_IS || op->opcode == ZEND_FETCH_OBJ_IS) { + if (op->opcode == ZEND_FETCH_DIM_IS) { + ZVAL_NULL(EX_VAR_NUM(i)); + } else if (op->opcode == ZEND_FETCH_OBJ_IS + && (Z_PROP_FLAG_P(val) & (IS_PROP_LAZY|IS_PROP_UNINIT)) == IS_PROP_UNINIT) { ZVAL_NULL(EX_VAR_NUM(i)); } else { - ZEND_ASSERT(op->opcode == ZEND_FETCH_DIM_R || op->opcode == ZEND_FETCH_LIST_R || op->opcode == ZEND_FETCH_OBJ_R || op->opcode == ZEND_FETCH_DIM_FUNC_ARG || op->opcode == ZEND_FETCH_OBJ_FUNC_ARG); + ZEND_ASSERT(op->opcode == ZEND_FETCH_DIM_R || op->opcode == ZEND_FETCH_LIST_R || op->opcode == ZEND_FETCH_OBJ_R || op->opcode == ZEND_FETCH_OBJ_IS || op->opcode == ZEND_FETCH_DIM_FUNC_ARG || op->opcode == ZEND_FETCH_OBJ_FUNC_ARG); repeat_last_opline = 1; } } else { diff --git a/ext/opcache/tests/jit/gh23628_001.phpt b/ext/opcache/tests/jit/gh23628_001.phpt new file mode 100644 index 000000000000..430cfbc8dc97 --- /dev/null +++ b/ext/opcache/tests/jit/gh23628_001.phpt @@ -0,0 +1,72 @@ +--TEST-- +GH-23628 001: Tracing JIT reads undefined property slots of a lazy proxy +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=tracing +opcache.jit_buffer_size=32M +opcache.jit_hot_loop=16 +--EXTENSIONS-- +opcache +--FILE-- + ['next' => 1]]; + public int $count = 0; + public function parse(int $n): int { + $ok = 0; + for ($i = 0; $i < $n; $i++) { + if (isset($this->map['start']['next'])) { + $ok++; + } else { + throw new RuntimeException('isset false at ' . $i); + } + } + return $ok; + } + public function coalesce(int $n): int { + $sum = 0; + for ($i = 0; $i < $n; $i++) { + $sum += $this->map['start']['next'] ?? 100; + } + return $sum; + } + public function read(int $n): int { + $sum = 0; + for ($i = 0; $i < $n; $i++) { + $sum += $this->map['start']['next']; + } + return $sum; + } + public function write(int $n): int { + for ($i = 0; $i < $n; $i++) { + $this->map['start']['next'] = $i; + $this->count++; + } + return $this->map['start']['next']; + } +} + +$reflector = new ReflectionClass(Table::class); + +$proxy = $reflector->newLazyProxy(fn () => new Table()); +var_dump($proxy->parse(100)); +$proxy = $reflector->newLazyProxy(fn () => new Table()); +var_dump($proxy->coalesce(100)); +$proxy = $reflector->newLazyProxy(fn () => new Table()); +var_dump($proxy->read(100)); +$proxy = $reflector->newLazyProxy(fn () => new Table()); +var_dump($proxy->write(100)); +var_dump($proxy->count); + +$ghost = $reflector->newLazyGhost(function (Table $table) {}); +var_dump($ghost->parse(100)); +?> +--EXPECT-- +int(100) +int(100) +int(100) +int(99) +int(100) +int(100) diff --git a/ext/opcache/tests/jit/gh23628_002.phpt b/ext/opcache/tests/jit/gh23628_002.phpt new file mode 100644 index 000000000000..12f306f8c5f1 --- /dev/null +++ b/ext/opcache/tests/jit/gh23628_002.phpt @@ -0,0 +1,46 @@ +--TEST-- +GH-23628 002: Tracing JIT deoptimization on an undefined property slot of a lazy proxy +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=tracing +opcache.jit_buffer_size=32M +opcache.jit_hot_loop=16 +--EXTENSIONS-- +opcache +--FILE-- + ['next' => 1]]; + public function parse(int $n): int { + $ok = 0; + for ($i = 0; $i < $n; $i++) { + if (isset($this->map['start']['next'])) { + $ok++; + } else { + throw new RuntimeException('isset false at ' . $i); + } + } + return $ok; + } +} + +// The trace is recorded and compiled for a regular object, so that the +// property is read directly from the property slot... +var_dump((new Table())->parse(100)); + +// ... and later executed for a lazy proxy, whose property slot is undefined +// and has to be forwarded to the real instance during deoptimization. +$proxy = (new ReflectionClass(Table::class))->newLazyProxy(fn () => new Table()); +var_dump($proxy->parse(100)); + +// ... and for an uninitialized lazy ghost, which is initialized on the first +// property access. +$ghost = (new ReflectionClass(Table::class))->newLazyGhost(function (Table $table) {}); +var_dump($ghost->parse(100)); +?> +--EXPECT-- +int(100) +int(100) +int(100) diff --git a/ext/opcache/tests/jit/gh23628_003.phpt b/ext/opcache/tests/jit/gh23628_003.phpt new file mode 100644 index 000000000000..a2d4654791be --- /dev/null +++ b/ext/opcache/tests/jit/gh23628_003.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-23628 003: Tracing JIT deoptimization on an unset() property served by __isset()/__get() +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=tracing +opcache.jit_buffer_size=32M +opcache.jit_hot_loop=16 +--EXTENSIONS-- +opcache +--FILE-- + 1]; + public function __isset($n) { return true; } + public function __get($n) { return ['x' => 42]; } + function f($n) { + $s = 0; + for ($i = 0; $i < $n; $i++) { + $s += $this->p['x'] ?? 1000; + } + return $s; + } +} + +// The trace is recorded and compiled while the property is initialized... +var_dump((new A)->f(100)); + +// ... and then executed after the property was unset(), so the fetch has to +// go through __isset()/__get() instead of yielding NULL. +$a = new A; +unset($a->p); +var_dump($a->f(100)); +?> +--EXPECT-- +int(100) +int(4200) From af8e9fb0844aa7f77acfd9f91bc85dd57e644c44 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:36:02 +0200 Subject: [PATCH 6/7] [ci skip] Fix test failure --- Zend/tests/lazy_objects/oss_fuzz_536440507.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt b/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt index 02a1fcc69342..1a818d458d6e 100644 --- a/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt +++ b/Zend/tests/lazy_objects/oss_fuzz_536440507.phpt @@ -17,5 +17,5 @@ var_dump($o->b); ?> --EXPECTF-- -Deprecated: Implicit conversion from float 3.4028236692093845E+32 to int loses precision in %s on line %d +Warning: The float 3.4028236692093845E+32 is not representable as an int, cast occurred in %s on line %d int(%s) From a8caefcafbb2e761c5edbe0ddf12e600d3de2062 Mon Sep 17 00:00:00 2001 From: mehmetcan <2010841+mehmetcansahin@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:22:09 +0300 Subject: [PATCH 7/7] array_splice(): Presize the result when nothing is removed (#23886) The output hash was sized for the replacement only, so copying the kept input elements grew it by doubling. Size it for the final element count. --- NEWS | 4 ++++ UPGRADING | 4 ++++ ext/standard/array.c | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 01eb73754ac1..2aaa3c4c3876 100644 --- a/NEWS +++ b/NEWS @@ -6,4 +6,8 @@ PHP NEWS . Fixed Collator attribute and strength methods not rejecting an unconstructed Collator. (Ilia Alshanetsky) +- Standard: + . Improved performance of array_splice() when inserting without removing + elements. (mehmetcansahin) + <<< NOTE: Insert NEWS from last stable release here prior to actual release! >>> diff --git a/UPGRADING b/UPGRADING index f66d5b6987f4..8c9bc4dac3f0 100644 --- a/UPGRADING +++ b/UPGRADING @@ -70,3 +70,7 @@ PHP 8.7 UPGRADE NOTES ======================================== 14. Performance Improvements ======================================== + +- Standard: + . Improved performance of array_splice() when inserting without removing + elements. diff --git a/ext/standard/array.c b/ext/standard/array.c index 4c3704c85106..a434589771b3 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3280,8 +3280,13 @@ static void php_splice(HashTable *in_hash, zend_long offset, zend_long length, H length = num_in - offset; } + /* Number of entries in the output hash: the input entries that are kept + * plus the replacement entries. After clamping, a non-positive length + * removes nothing, so all input entries are kept. */ + uint32_t num_out = num_in - MAX(length, 0) + (replace ? zend_hash_num_elements(replace) : 0); + /* Create and initialize output hash */ - zend_hash_init(&out_hash, (length > 0 ? num_in - length : 0) + (replace ? zend_hash_num_elements(replace) : 0), NULL, ZVAL_PTR_DTOR, 0); + zend_hash_init(&out_hash, num_out, NULL, ZVAL_PTR_DTOR, 0); if (HT_IS_PACKED(in_hash)) { /* Start at the beginning of the input hash and copy entries to output hash until offset is reached */