From 7300fa520dd38dadf9563a9104aa0036cb864147 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 10:36:56 -0400 Subject: [PATCH 01/16] ext/sqlite3: reject close() from inside a callback SQLite3::close() called from within a userland function, aggregate, collation or authorizer callback freed the registered statements and functions while sqlite3 was still executing, corrupting the active statement and crashing the request. Track callback re-entry with a per-database counter shared by all four callback kinds and throw an Error from close() while it is non-zero; the database stays usable and can be closed after the query completes. Closes GH-23650 --- NEWS | 4 ++ ext/sqlite3/php_sqlite3_structs.h | 3 ++ ext/sqlite3/sqlite3.c | 52 +++++++++++++++++++ .../tests/sqlite3_close_from_callback.phpt | 35 +++++++++++++ ...sqlite3_close_from_callback_aggregate.phpt | 41 +++++++++++++++ ...qlite3_close_from_callback_authorizer.phpt | 38 ++++++++++++++ ...sqlite3_close_from_callback_collation.phpt | 39 ++++++++++++++ 7 files changed, 212 insertions(+) create mode 100644 ext/sqlite3/tests/sqlite3_close_from_callback.phpt create mode 100644 ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt create mode 100644 ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt create mode 100644 ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt diff --git a/NEWS b/NEWS index 9a862b7e0e1d..50815738590c 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,10 @@ PHP NEWS . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- SQLite: + . Fixed a crash when SQLite3::close() is called from a userland callback. + (Ilia Alshanetsky) + - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) diff --git a/ext/sqlite3/php_sqlite3_structs.h b/ext/sqlite3/php_sqlite3_structs.h index 6d445d6642db..43e595affe1b 100644 --- a/ext/sqlite3/php_sqlite3_structs.h +++ b/ext/sqlite3/php_sqlite3_structs.h @@ -50,6 +50,7 @@ typedef struct _php_sqlite3_func { zend_fcall_info_cache func; zend_fcall_info_cache step; zend_fcall_info_cache fini; + unsigned int *in_callback_ptr; } php_sqlite3_func; /* Structure for SQLite collation function */ @@ -58,6 +59,7 @@ typedef struct _php_sqlite3_collation { const char *collation_name; zend_fcall_info_cache cmp_func; + unsigned int *in_callback_ptr; } php_sqlite3_collation; /* Structure for SQLite Database object. */ @@ -69,6 +71,7 @@ typedef struct _php_sqlite3_db_object { zend_fcall_info_cache authorizer_fcc; bool exception; + unsigned int in_callback; zend_llist free_list; zend_object zo; diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 20714c38aada..93e50b97c7ff 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -191,6 +191,10 @@ PHP_METHOD(SQLite3, close) } if (db_obj->initialised) { + if (db_obj->in_callback) { + zend_throw_error(NULL, "Cannot close SQLite3 database while inside a callback"); + RETURN_THROWS(); + } zend_llist_clean(&(db_obj->free_list)); if(db_obj->db) { errcode = sqlite3_close(db_obj->db); @@ -774,6 +778,9 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite uint32_t fake_argc; zend_result ret = SUCCESS; php_sqlite3_agg_context *agg_context = NULL; + bool bailout = false; + php_sqlite3_func *cb_func = (php_sqlite3_func *)sqlite3_user_data(context); + unsigned int *in_callback = cb_func ? cb_func->in_callback_ptr : NULL; if (is_agg) { is_agg = 2; @@ -781,6 +788,10 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite fake_argc = argc + is_agg; + if (in_callback) { + (*in_callback)++; + } + /* build up the params */ if (fake_argc) { zargs = (zval *)safe_emalloc(fake_argc, sizeof(zval), 0); @@ -823,7 +834,15 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite } } + zend_try { zend_call_known_fcc(fcc, &retval, fake_argc, zargs, /* named_params */ NULL); + } zend_catch { + bailout = true; + } zend_end_try(); + + if (in_callback) { + (*in_callback)--; + } /* clean up the params */ if (is_agg) { @@ -889,6 +908,9 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite if (!Z_ISUNDEF(retval)) { zval_ptr_dtor(&retval); } + if (bailout) { + zend_bailout(); + } return ret; } /* }}}*/ @@ -929,6 +951,7 @@ static int php_sqlite3_callback_compare(void *coll, int a_len, const void *a, in zval zargs[2]; zval retval; int ret = 0; + bool bailout = false; // Exception occurred on previous callback. Don't attempt to call function. if (EG(exception)) { @@ -938,10 +961,26 @@ static int php_sqlite3_callback_compare(void *coll, int a_len, const void *a, in ZVAL_STRINGL(&zargs[0], a, a_len); ZVAL_STRINGL(&zargs[1], b, b_len); + if (collation->in_callback_ptr) { + (*collation->in_callback_ptr)++; + } + + zend_try { zend_call_known_fcc(&collation->cmp_func, &retval, /* argc */ 2, zargs, /* named_params */ NULL); + } zend_catch { + bailout = true; + } zend_end_try(); + + if (collation->in_callback_ptr) { + (*collation->in_callback_ptr)--; + } zval_ptr_dtor(&zargs[0]); zval_ptr_dtor(&zargs[1]); + if (bailout) { + zval_ptr_dtor(&retval); + zend_bailout(); + } if (EG(exception)) { ret = 0; @@ -988,6 +1027,7 @@ PHP_METHOD(SQLite3, createFunction) } func = (php_sqlite3_func *)ecalloc(1, sizeof(*func)); + func->in_callback_ptr = &db_obj->in_callback; if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, flags | SQLITE_UTF8, func, php_sqlite3_callback_func, NULL, NULL) == SQLITE_OK) { func->func_name = estrdup(sql_func); @@ -1037,6 +1077,7 @@ PHP_METHOD(SQLite3, createAggregate) } func = (php_sqlite3_func *)ecalloc(1, sizeof(*func)); + func->in_callback_ptr = &db_obj->in_callback; if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, SQLITE_UTF8, func, NULL, php_sqlite3_callback_step, php_sqlite3_callback_final) == SQLITE_OK) { func->func_name = estrdup(sql_func); @@ -1085,6 +1126,7 @@ PHP_METHOD(SQLite3, createCollation) } collation = (php_sqlite3_collation *)ecalloc(1, sizeof(*collation)); + collation->in_callback_ptr = &db_obj->in_callback; if (sqlite3_create_collation(db_obj->db, collation_name, SQLITE_UTF8, collation, php_sqlite3_callback_compare) == SQLITE_OK) { collation->collation_name = estrdup(collation_name); @@ -2151,8 +2193,15 @@ static int php_sqlite3_authorizer(void *autharg, int action, const char *arg1, c } int authreturn = SQLITE_DENY; + bool bailout = false; + db_obj->in_callback++; + zend_try { zend_call_known_fcc(&db_obj->authorizer_fcc, &retval, /* argc */ 5, argv, /* named_params */ NULL); + } zend_catch { + bailout = true; + } zend_end_try(); + db_obj->in_callback--; if (Z_ISUNDEF(retval)) { php_sqlite3_error(db_obj, 0, "An error occurred while invoking the authorizer callback"); } else { @@ -2176,6 +2225,9 @@ static int php_sqlite3_authorizer(void *autharg, int action, const char *arg1, c zval_ptr_dtor(&argv[3]); zval_ptr_dtor(&argv[4]); + if (bailout) { + zend_bailout(); + } return authreturn; } /* }}} */ diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback.phpt new file mode 100644 index 000000000000..0b5523717f76 --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_close_from_callback.phpt @@ -0,0 +1,35 @@ +--TEST-- +SQLite3::close() from within a UDF callback must not corrupt active statement +--EXTENSIONS-- +sqlite3 +--FILE-- +createFunction('boom', function () use ($db) { + try { + var_dump($db->close()); + } catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + return 1; +}); +$stmt = $db->prepare('SELECT boom()'); +var_dump($stmt !== false); +$res = $stmt->execute(); +var_dump($res !== false); +var_dump($res->fetchArray(SQLITE3_NUM)); +$res->finalize(); +var_dump($db->close()); +echo "done\n"; +?> +--EXPECT-- +bool(true) +Error: Cannot close SQLite3 database while inside a callback +bool(true) +Error: Cannot close SQLite3 database while inside a callback +array(1) { + [0]=> + int(1) +} +bool(true) +done diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt new file mode 100644 index 000000000000..b5bdb9615a78 --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt @@ -0,0 +1,41 @@ +--TEST-- +SQLite3::close() from within a createAggregate() callback must not corrupt active statement +--EXTENSIONS-- +sqlite3 +--FILE-- +exec('CREATE TABLE t (a INTEGER)'); +$db->exec('INSERT INTO t VALUES (1)'); +$db->exec('INSERT INTO t VALUES (2)'); + +$reported = false; +$db->createAggregate('agg', function ($context, $rows, $a) use (&$db, &$reported) { + if (!$reported) { + $reported = true; + try { + var_dump($db->close()); + } catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } + return (int) $context + $a; +}, function ($context, $rows) { + return $context; +}, 1); + +$stmt = $db->prepare('SELECT agg(a) FROM t'); +$res = $stmt->execute(); +var_dump($res->fetchArray(SQLITE3_NUM)); +$res->finalize(); +var_dump($db->close()); +echo "done\n"; +?> +--EXPECT-- +Error: Cannot close SQLite3 database while inside a callback +array(1) { + [0]=> + int(3) +} +bool(true) +done diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt new file mode 100644 index 000000000000..1d5cf9e49227 --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt @@ -0,0 +1,38 @@ +--TEST-- +SQLite3::close() from within a setAuthorizer() callback must not finalize live statements +--EXTENSIONS-- +sqlite3 +--FILE-- +exec('CREATE TABLE t (a INTEGER)'); +$db->exec('INSERT INTO t VALUES (7)'); +$live = $db->prepare('SELECT a FROM t'); + +$reported = false; +$db->setAuthorizer(function ($action, $arg1, $arg2, $arg3, $arg4) use (&$db, &$reported) { + if (!$reported) { + $reported = true; + try { + var_dump($db->close()); + } catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } + return SQLite3::OK; +}); + +$db->prepare('SELECT 1'); + +$res = $live->execute(); +var_dump($res->fetchArray(SQLITE3_NUM)); +$res->finalize(); +echo "done\n"; +?> +--EXPECT-- +Error: Cannot close SQLite3 database while inside a callback +array(1) { + [0]=> + int(7) +} +done diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt new file mode 100644 index 000000000000..16906894161c --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt @@ -0,0 +1,39 @@ +--TEST-- +SQLite3::close() from within a createCollation() callback must not corrupt active statement +--EXTENSIONS-- +sqlite3 +--FILE-- +exec('CREATE TABLE t (s TEXT)'); +$db->exec("INSERT INTO t VALUES ('b')"); +$db->exec("INSERT INTO t VALUES ('a')"); + +$reported = false; +$db->createCollation('CB', function ($x, $y) use (&$db, &$reported) { + if (!$reported) { + $reported = true; + try { + var_dump($db->close()); + } catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } + return strcmp($x, $y); +}); + +$stmt = $db->prepare('SELECT s FROM t ORDER BY s COLLATE CB'); +$res = $stmt->execute(); +while ($row = $res->fetchArray(SQLITE3_NUM)) { + echo $row[0], "\n"; +} +$res->finalize(); +var_dump($db->close()); +echo "done\n"; +?> +--EXPECT-- +Error: Cannot close SQLite3 database while inside a callback +a +b +bool(true) +done From 92b08946a33e00c7b272acaa7a1c0240cd16edc6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 6 Sep 2026 10:56:24 -0400 Subject: [PATCH 02/16] dom: fix attribute namespaces from foreign content in the HTML parser The bridge registered an id as XML_ATTRIBUTE_ID only when lexbor reported the HTML namespace, so ids on SVG and MathML elements never reached getElementById(). Separately, lexbor initializes every attribute node with its element's namespace and overrides that only through the foreign-attribute adjust table, which the parser wires up for SVG and MathML alone, so a fragment parsed with an xlink, xml or xmlns context element came back as . Restrict the three namespace branches to attributes lexbor adjusted, and key the id registration off the namespace the bridge assigned. Closes GH-23598 --- NEWS | 5 ++ ext/dom/html5_parser.c | 8 +-- ...refixed_attributes_in_foreign_content.phpt | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt diff --git a/NEWS b/NEWS index 50815738590c..69b3dec86ed5 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,11 @@ PHP NEWS (Ilia Alshanetsky) . Fixed bug GH-23365 (DOMNode::insertBefore($n, $n) drops the node and leaves a self-referencing sibling list). (David Carlier) + . Fixed Dom\HTMLDocument::getElementById() not finding ids of SVG and + MathML elements. (Ilia Alshanetsky) + . Fixed Dom\HTMLDocument giving attributes the namespace of their element + when a fragment is parsed with an xlink, xml or xmlns context element. + (Ilia Alshanetsky) - Intl: . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c index 34320a122f53..24f89cb0fafb 100644 --- a/ext/dom/html5_parser.c +++ b/ext/dom/html5_parser.c @@ -241,7 +241,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( lxml_attr->children = lxml_attr->last = lxml_text; lxml_text->parent = (xmlNodePtr) lxml_attr; - if (attr->node.ns == LXB_NS_XMLNS) { + if (attr->node.ns == LXB_NS_XMLNS && (attr->node.prefix || strcmp((const char *) local_name, "xmlns") == 0)) { if (strcmp((const char *) local_name, "xmlns") != 0) { if (prefixed_xmlns_ns == NULL) { prefixed_xmlns_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xmlns", DOM_XMLNS_NS_URI); @@ -251,13 +251,13 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( lxml_attr->ns = php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(ns_mapper); } lxml_attr->ns->_private = (void *) php_dom_ns_is_xmlns_magic_token; - } else if (attr->node.ns == LXB_NS_XLINK) { + } else if (attr->node.prefix && attr->node.ns == LXB_NS_XLINK) { if (xlink_ns == NULL) { xlink_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xlink", DOM_XLINK_NS_URI); xlink_ns->_private = (void *) php_dom_ns_is_xlink_magic_token; } lxml_attr->ns = xlink_ns; - } else if (attr->node.ns == LXB_NS_XML) { + } else if (attr->node.prefix && attr->node.ns == LXB_NS_XML) { if (xml_ns == NULL) { xml_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xml", DOM_XML_NS_URI); xml_ns->_private = (void *) php_dom_ns_is_xml_magic_token; @@ -274,7 +274,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( last_added_attr = lxml_attr; /* xmlIsID does some other stuff too that is irrelevant here. */ - if (local_name_length == 2 && local_name[0] == 'i' && local_name[1] == 'd' && attr->node.ns == LXB_NS_HTML) { + if (local_name_length == 2 && local_name[0] == 'i' && local_name[1] == 'd' && lxml_attr->ns == NULL) { if (xmlAddID(NULL, lxml_doc, value, lxml_attr) == 0) { /* If the ID already exists, the ID attribute still needs to be marked as an ID. */ lxml_attr->atype = XML_ATTRIBUTE_ID; diff --git a/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt b/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt new file mode 100644 index 000000000000..ba6d33fdbbc5 --- /dev/null +++ b/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt @@ -0,0 +1,59 @@ +--TEST-- +Unprefixed attributes in foreign content are in no namespace +--EXTENSIONS-- +dom +--FILE-- +

'; +$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); + +var_dump([ + 'svg #s' => $doc->getElementById('s')?->tagName, + 'math #m' => $doc->getElementById('m')?->tagName, + 'html #p' => $doc->getElementById('p')?->tagName, + 'xml:id #r' => $doc->getElementById('r')?->tagName, +]); + +foreach ($doc->getElementById('s')->attributes as $attr) { + echo $attr->name, ' => ', var_export($attr->namespaceURI, true), "\n"; +} + +$contexts = [ + 'http://www.w3.org/1999/xlink' => 'z', + 'http://www.w3.org/XML/1998/namespace' => 'z', + 'http://www.w3.org/2000/xmlns/' => 'xmlns', +]; +foreach ($contexts as $uri => $name) { + $fragment_doc = Dom\HTMLDocument::createEmpty(); + $context = $fragment_doc->createElementNS($uri, $name); + $fragment_doc->appendChild($context); + $context->innerHTML = ''; + + echo $uri, "\n ", $context->innerHTML, "\n "; + var_dump($fragment_doc->getElementById('q')?->tagName); +} +?> +--EXPECT-- +array(4) { + ["svg #s"]=> + string(3) "svg" + ["math #m"]=> + string(4) "math" + ["html #p"]=> + string(1) "P" + ["xml:id #r"]=> + NULL +} +id => NULL +xlink:href => 'http://www.w3.org/1999/xlink' +xml:lang => 'http://www.w3.org/XML/1998/namespace' +xmlns:xlink => 'http://www.w3.org/2000/xmlns/' +http://www.w3.org/1999/xlink + + string(1) "Z" +http://www.w3.org/XML/1998/namespace + + string(1) "Z" +http://www.w3.org/2000/xmlns/ + + string(1) "Z" From 2bca7364790124b75fbfc9e10b08277e3744cafd Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 17 Sep 2026 20:06:08 +0100 Subject: [PATCH 03/16] ext/sockets: socket_select() silent set truncation on Windows. php_sock_array_to_fd_set() returned a flag rather than the socket count, so PHP_SAFE_MAX_FD() got a zero and never fired. Sets larger than FD_SETSIZE were quietly truncated and select() reported on the survivors. Close GH-23632 --- NEWS | 4 ++ ext/sockets/sockets.c | 23 ++++++--- .../socket_select_fd_setsize_overflow.phpt | 50 +++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 ext/sockets/tests/socket_select_fd_setsize_overflow.phpt diff --git a/NEWS b/NEWS index 69b3dec86ed5..42534ef4c376 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,10 @@ PHP NEWS . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- Sockets: + . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on + Windows. (David Carlier) + - SQLite: . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 565a2d791bca..5e70df60726e 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -542,7 +542,7 @@ static int php_sock_array_to_fd_set(uint32_t arg_num, zval *sock_array, fd_set * num++; } ZEND_HASH_FOREACH_END(); - return num ? 1 : 0; + return num; } /* }}} */ @@ -593,7 +593,7 @@ PHP_FUNCTION(socket_select) struct timeval *tv_p = NULL; fd_set rfds, wfds, efds; PHP_SOCKET max_fd = 0; - int retval, sets = 0; + int retval, max_set_count = 0; zend_long sec, usec = 0; bool sec_is_null = 0; @@ -611,30 +611,39 @@ PHP_FUNCTION(socket_select) FD_ZERO(&efds); if (r_array != NULL) { - sets += retval = php_sock_array_to_fd_set(1, r_array, &rfds, &max_fd); + retval = php_sock_array_to_fd_set(1, r_array, &rfds, &max_fd); if (retval == -1) { RETURN_THROWS(); } + if (retval > max_set_count) { + max_set_count = retval; + } } if (w_array != NULL) { - sets += retval = php_sock_array_to_fd_set(2, w_array, &wfds, &max_fd); + retval = php_sock_array_to_fd_set(2, w_array, &wfds, &max_fd); if (retval == -1) { RETURN_THROWS(); } + if (retval > max_set_count) { + max_set_count = retval; + } } if (e_array != NULL) { - sets += retval = php_sock_array_to_fd_set(3, e_array, &efds, &max_fd); + retval = php_sock_array_to_fd_set(3, e_array, &efds, &max_fd); if (retval == -1) { RETURN_THROWS(); } + if (retval > max_set_count) { + max_set_count = retval; + } } - if (!sets) { + if (!max_set_count) { zend_value_error("socket_select(): At least one array argument must be passed"); RETURN_THROWS(); } - if (!PHP_SAFE_MAX_FD(max_fd, 0)) { + if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) { RETURN_FALSE; } diff --git a/ext/sockets/tests/socket_select_fd_setsize_overflow.phpt b/ext/sockets/tests/socket_select_fd_setsize_overflow.phpt new file mode 100644 index 000000000000..bb20d0df919a --- /dev/null +++ b/ext/sockets/tests/socket_select_fd_setsize_overflow.phpt @@ -0,0 +1,50 @@ +--TEST-- +socket_select() reports an overflowing set instead of silently truncating it +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + 256); + +$read = $sockets; +$write = $except = null; +var_dump(socket_select($read, $write, $except, 0)); + +$write = $sockets; +$read = $except = null; +var_dump(socket_select($read, $write, $except, 0)); + +$except = $sockets; +$read = $write = null; +var_dump(socket_select($read, $write, $except, 0)); + +foreach ($sockets as $socket) { + socket_close($socket); +} +?> +--EXPECTF-- +bool(true) + +Warning: socket_select(): PHP needs to be recompiled with a larger value of FD_SETSIZE.%A +bool(false) + +Warning: socket_select(): PHP needs to be recompiled with a larger value of FD_SETSIZE.%A +bool(false) + +Warning: socket_select(): PHP needs to be recompiled with a larger value of FD_SETSIZE.%A +bool(false) From b55c619356c810144a68596f5b15c7d9f1354b73 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 17 Sep 2026 20:03:18 +0100 Subject: [PATCH 04/16] ext/xsl: XSLTProcessor::importStylesheet() use-after-free during a transformation. Fix #23730 Importing a stylesheet from a php:function callback freed the stylesheet libxslt was still applying, and the transformation methods kept using the stale pointer to save the result. The transformation depth is now tracked on the object and importStylesheet() throws while it is non-zero. Close GH-23737 --- NEWS | 4 ++ ext/xsl/php_xsl.h | 1 + ext/xsl/tests/gh23730.phpt | 81 ++++++++++++++++++++++++++++++++++++++ ext/xsl/xsltprocessor.c | 12 +++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 ext/xsl/tests/gh23730.phpt diff --git a/NEWS b/NEWS index 42534ef4c376..c38d108d840a 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,10 @@ PHP NEWS . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) +- XSL: + . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() + is called during a transformation). (David Carlier) + - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) diff --git a/ext/xsl/php_xsl.h b/ext/xsl/php_xsl.h index 36bd9cc72844..ac4ac5fc2fba 100644 --- a/ext/xsl/php_xsl.h +++ b/ext/xsl/php_xsl.h @@ -55,6 +55,7 @@ extern zend_module_entry xsl_module_entry; typedef struct xsl_object { void *ptr; HashTable *parameter; + uint32_t transform_depth; bool hasKeys; php_libxml_ref_obj *sheet_ref_obj; zend_long securityPrefs; diff --git a/ext/xsl/tests/gh23730.phpt b/ext/xsl/tests/gh23730.phpt new file mode 100644 index 000000000000..893f6277734b --- /dev/null +++ b/ext/xsl/tests/gh23730.phpt @@ -0,0 +1,81 @@ +--TEST-- +GH-23730 (Use-after-free when a stylesheet is imported during a transformation) +--EXTENSIONS-- +dom +xsl +--CREDITS-- +djarfluka +--FILE-- +importStylesheet($GLOBALS['other']); + echo $from, ': no error', PHP_EOL; + } catch (Error $e) { + echo $from, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +function callback($nodes) { + import_other('callback'); + return $nodes[0]; +} + +$xml = new DOMDocument(); +$xml->registerNodeClass(DOMElement::class, MyElement::class); +$xml->loadXML('a'); + +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + + +XML); + +$other = new DOMDocument(); +$other->loadXML('REPLACED'); + +$proc = new XSLTProcessor(); +$proc->registerPHPFunctions(); +$proc->importStylesheet($xsl); + +$GLOBALS['proc'] = $proc; +$GLOBALS['other'] = $other; + +var_dump($proc->transformToXml($xml)); +var_dump($proc->transformToDoc($xml)->textContent); + +$uri = tempnam(sys_get_temp_dir(), 'gh23730'); +var_dump($proc->transformToUri($xml, $uri) > 0); +@unlink($uri); + +/* Importing outside of a transformation is still allowed. */ +var_dump($proc->importStylesheet($other)); +var_dump($proc->transformToXml($xml)); + +?> +--EXPECT-- +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(24) " +a +" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(1) "a" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +bool(true) +bool(true) +string(31) " +REPLACED +" diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index cf5a941d95ca..fa6b2d41e601 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -175,6 +175,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_THROWS(); } + xsl_object *intern = Z_XSL_P(id); + if (UNEXPECTED(intern->transform_depth > 0)) { + zend_throw_error(NULL, "Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress"); + RETURN_THROWS(); + } + nodep = php_libxml_import_node(docp); if (nodep == NULL) { zend_argument_type_error(1, "must be a valid XML node"); @@ -251,8 +257,6 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_FALSE; } - xsl_object *intern = Z_XSL_P(id); - /* Detach object */ clone_lxml_obj->document->ptr = NULL; /* The namespace mappings need to be kept alive. @@ -333,6 +337,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl return NULL; } + intern->transform_depth++; + if (intern->profiling) { if (php_check_open_basedir(ZSTR_VAL(intern->profiling))) { f = NULL; @@ -438,6 +444,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl efree(intern->doc); intern->doc = NULL; + intern->transform_depth--; + return newdocp; } From 132403de39604a7e7a3fb04276dde54d6326c9aa Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 18 Sep 2026 05:04:30 +0100 Subject: [PATCH 05/16] ext/zip: ZipArchive::close() use-after-free from a progress or cancel callback. Fix #23747 ZipArchive::close() called from a progress or cancel callback ran a nested zip_close() that failed, then zip_discard() freed the archive while the outer zip_close() from close() or open() was still using it. Track the close in progress and throw an Error from close() and open() meanwhile. Close GH-23749 --- NEWS | 2 + ext/zip/php_zip.c | 17 ++++++- ext/zip/php_zip.h | 1 + ext/zip/tests/gh23747.phpt | 102 +++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 ext/zip/tests/gh23747.phpt diff --git a/NEWS b/NEWS index c38d108d840a..c8eb609cc2a6 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,8 @@ PHP NEWS - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) + . Fixed bug GH-23747 (ZipArchive::close() use-after-free from a progress or + cancel callback). (David Carlier) 24 Sep 2026, PHP 8.4.26 diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 682736d4fe76..42a5a527cfc7 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -1582,8 +1582,16 @@ PHP_METHOD(ZipArchive, open) if (ze_obj->archive) { /* we already have an opened zip, free it */ + if (ze_obj->archive->close) { + efree(resolved_path); + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } intern = ze_obj->archive->za; - if (zip_close(intern) != 0) { + ze_obj->archive->close = true; + err = zip_close(intern); + ze_obj->archive->close = false; + if (err != 0) { php_error_docref(NULL, E_WARNING, "Empty string as source"); efree(resolved_path); RETURN_FALSE; @@ -1668,7 +1676,14 @@ PHP_METHOD(ZipArchive, close) ze_obj = Z_ZIP_P(self); + if (ze_obj->archive->close) { + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } + + ze_obj->archive->close = true; err = zip_close(intern); + ze_obj->archive->close = false; if (err) { php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern)); /* Save error for property reader */ diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h index 8385674a1cf0..e5930c87aada 100644 --- a/ext/zip/php_zip.h +++ b/ext/zip/php_zip.h @@ -73,6 +73,7 @@ typedef struct _php_zip_archive { /* libzip reads buffers until the archive is closed, can outlive the object. */ char **buffers; int buffers_cnt; + bool close; #ifdef HAVE_PROGRESS_CALLBACK zval progress_callback; #endif diff --git a/ext/zip/tests/gh23747.phpt b/ext/zip/tests/gh23747.phpt new file mode 100644 index 000000000000..24e977410e17 --- /dev/null +++ b/ext/zip/tests/gh23747.phpt @@ -0,0 +1,102 @@ +--TEST-- +GH-23747 (ZipArchive::close() from inside a progress or cancel callback causes segv) +--CREDITS-- +djarfluka +--EXTENSIONS-- +zip +--SKIPIF-- + +--FILE-- +open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); + for ($i = 0; $i < 64; $i++) { + $zip->addFromString("f$i.txt", str_repeat('x', 2000)); + } +} + +$filename = __DIR__ . '/gh23747.zip'; + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerCancelCallback(function () use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } + return 0; +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->open($filename)); +var_dump($zip->count()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip, $filename) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->open($filename); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); +?> +--CLEAN-- + +--EXPECT-- +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +int(64) +Error: Already being closed +bool(true) From a74f826ff2979a4d46d7c69b78fbb86c0f173373 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 17 Sep 2026 19:40:13 +0100 Subject: [PATCH 06/16] Fix GH-23729: DOMXPath::__construct() use-after-free during an evaluation Reconstructing the object from a php:function callback freed the context libxml2 was still evaluating, and php_xpath_eval() wrote back into it once the evaluation returned. The evaluation depth is now tracked on the object and __construct() throws while it is non-zero. Close GH-23735 --- NEWS | 2 ++ ext/dom/php_dom.h | 1 + ext/dom/tests/gh23729.phpt | 73 ++++++++++++++++++++++++++++++++++++++ ext/dom/xpath.c | 10 +++++- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/gh23729.phpt diff --git a/NEWS b/NEWS index c8eb609cc2a6..46824ac7d6ae 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,8 @@ PHP NEWS . Fixed Dom\HTMLDocument giving attributes the namespace of their element when a fragment is parsed with an xlink, xml or xmlns context element. (Ilia Alshanetsky) + . Fixed bug GH-23729 (DOMXPath::__construct() use-after-free during an + evaluation). (David Carlier) - Intl: . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 13f49879bb38..d399d745084b 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -66,6 +66,7 @@ extern zend_module_entry dom_module_entry; typedef struct dom_xpath_object { php_dom_xpath_callbacks xpath_callbacks; + uint32_t evaluation_depth; bool register_node_ns; dom_object dom; } dom_xpath_object; diff --git a/ext/dom/tests/gh23729.phpt b/ext/dom/tests/gh23729.phpt new file mode 100644 index 000000000000..93ad67fde005 --- /dev/null +++ b/ext/dom/tests/gh23729.phpt @@ -0,0 +1,73 @@ +--TEST-- +GH-23729 (Use-after-free when DOMXPath is reconstructed during an evaluation) +--CREDITS-- +djarfluka +--EXTENSIONS-- +dom +--FILE-- +__construct($GLOBALS['other']); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + return 'r'; +} + +function nested() { + echo 'nested: ', $GLOBALS['xpath']->evaluate('string(/root/b)'), PHP_EOL; + return 'n'; +} + +function test(string $class, object $doc, object $other) { + $xpath = new $class($doc); + $xpath->registerNamespace('php', 'http://php.net/xpath'); + $xpath->registerPhpFunctions(); + + $GLOBALS['xpath'] = $xpath; + $GLOBALS['other'] = $other; + + var_dump($xpath->evaluate('string(php:function("reconstruct"))')); + /* The evaluation the callback tried to destroy must still be usable. */ + var_dump($xpath->evaluate('string(/root/a)')); + /* A nested evaluation must not lift the guard of the outer one. */ + var_dump($xpath->evaluate('concat(php:function("nested"), php:function("reconstruct"))')); + var_dump($xpath->query('//b[php:function("reconstruct")]')->length); + + /* Reconstructing outside of an evaluation is still allowed. */ + $xpath->__construct($other); + var_dump($xpath->document->documentElement->nodeName); +} + +$doc = new DOMDocument(); +$doc->loadXML('12'); +$other = new DOMDocument(); +$other->loadXML(''); +test(DOMXPath::class, $doc, $other); + +$doc = Dom\XMLDocument::createFromString('12'); +$other = Dom\XMLDocument::createFromString(''); +test(Dom\XPath::class, $doc, $other); + +?> +--EXPECT-- +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index 4f1b3b52714e..d4367c9aaa60 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -126,6 +126,13 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + if (UNEXPECTED(intern->evaluation_depth > 0)) { + zend_throw_error(NULL, "Cannot call %s::__construct() while an XPath evaluation is in progress", + ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); + RETURN_THROWS(); + } + DOM_GET_OBJ(docp, doc, xmlDocPtr, docobj); xmlXPathContextPtr ctx = xmlXPathNewContext(docp); @@ -134,7 +141,6 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } - dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); xmlXPathContextPtr oldctx = intern->dom.ptr; if (oldctx != NULL) { php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom); @@ -301,7 +307,9 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) ctxp->nsNr = in_scope_ns.count; } + intern->evaluation_depth++; xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); + intern->evaluation_depth--; ctxp->node = NULL; if (register_node_ns && nodep != NULL) { From 6d910200db4b1826a84b442191ba616fbfb359b2 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 19 Sep 2026 14:06:08 +0100 Subject: [PATCH 07/16] Zend: reorganise some type declarations tests --- .../{typehints => type_declarations/iterable}/bug76198.phpt | 0 .../relative_types}/bug43332_1.phpt | 2 +- .../relative_types}/bug43332_2.phpt | 2 +- .../relative_types}/bug60573.phpt | 2 +- .../relative_types}/bug60573_2.phpt | 2 +- .../{ => relative_types}/parent_is_not_proto.phpt | 0 .../{ => relative_types}/self_on_closure_in_method.phpt | 0 .../{ => relative_types/static}/static_type_in_final_class.phpt | 0 .../{ => relative_types/static}/static_type_outside_class.phpt | 0 .../{ => relative_types/static}/static_type_param.phpt | 0 .../{ => relative_types/static}/static_type_property.phpt | 0 .../{ => relative_types/static}/static_type_return.phpt | 0 .../{ => relative_types/static}/static_type_trait.phpt | 0 13 files changed, 4 insertions(+), 4 deletions(-) rename Zend/tests/{typehints => type_declarations/iterable}/bug76198.phpt (100%) rename Zend/tests/{typehints => type_declarations/relative_types}/bug43332_1.phpt (86%) rename Zend/tests/{typehints => type_declarations/relative_types}/bug43332_2.phpt (80%) rename Zend/tests/{typehints => type_declarations/relative_types}/bug60573.phpt (90%) rename Zend/tests/{typehints => type_declarations/relative_types}/bug60573_2.phpt (90%) rename Zend/tests/type_declarations/{ => relative_types}/parent_is_not_proto.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types}/self_on_closure_in_method.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_in_final_class.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_outside_class.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_param.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_property.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_return.phpt (100%) rename Zend/tests/type_declarations/{ => relative_types/static}/static_type_trait.phpt (100%) diff --git a/Zend/tests/typehints/bug76198.phpt b/Zend/tests/type_declarations/iterable/bug76198.phpt similarity index 100% rename from Zend/tests/typehints/bug76198.phpt rename to Zend/tests/type_declarations/iterable/bug76198.phpt diff --git a/Zend/tests/typehints/bug43332_1.phpt b/Zend/tests/type_declarations/relative_types/bug43332_1.phpt similarity index 86% rename from Zend/tests/typehints/bug43332_1.phpt rename to Zend/tests/type_declarations/relative_types/bug43332_1.phpt index 35d817c985ee..7639633c3e95 100644 --- a/Zend/tests/typehints/bug43332_1.phpt +++ b/Zend/tests/type_declarations/relative_types/bug43332_1.phpt @@ -1,5 +1,5 @@ --TEST-- -Bug #43332.1 (self and parent as type hint in namespace) +Bug #43332.1 (self and parent as type declaration in namespace) --FILE-- Date: Sat, 19 Sep 2026 14:28:54 +0100 Subject: [PATCH 08/16] Zend: fix OSS-Fuzz 532353396, static is a built-in type And thus should never be allowed to be qualified, as this lead to other bugs such as it not considered to be a built-in type by Reflection if it had a namespace component. We *may* want to extend the unqualified restriction to self and parent in the future. Closes GH-23768 --- NEWS | 1 + .../static/fully_qualified_static.phpt | 12 ++++++++++++ .../static/namespace_relative_static.phpt | 12 ++++++++++++ Zend/zend_compile.c | 3 +++ 4 files changed, 28 insertions(+) create mode 100644 Zend/tests/type_declarations/relative_types/static/fully_qualified_static.phpt create mode 100644 Zend/tests/type_declarations/relative_types/static/namespace_relative_static.phpt diff --git a/NEWS b/NEWS index be02d0d58cce..5c2531405229 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ PHP NEWS - Core: . Fixed bug GH-23644 (Optimizer leaves a constant-vs-constant comparison unfolded, crashing the VM in zval_undefined_cv). (ndossche) + . Fixed OSS-Fuzz 532353396 (assertion failure with static type). (Girgias) - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function diff --git a/Zend/tests/type_declarations/relative_types/static/fully_qualified_static.phpt b/Zend/tests/type_declarations/relative_types/static/fully_qualified_static.phpt new file mode 100644 index 000000000000..4967e3f3d116 --- /dev/null +++ b/Zend/tests/type_declarations/relative_types/static/fully_qualified_static.phpt @@ -0,0 +1,12 @@ +--TEST-- +Fully qualified (leading backslash) static type names must fail +--DESCRIPTION-- +OSS-Fuzz: https://issues.oss-fuzz.com/issues/532353396 +--FILE-- + +--EXPECTF-- +Fatal error: Type declaration 'static' must be unqualified in %s on line %d diff --git a/Zend/tests/type_declarations/relative_types/static/namespace_relative_static.phpt b/Zend/tests/type_declarations/relative_types/static/namespace_relative_static.phpt new file mode 100644 index 000000000000..0c1cde76f147 --- /dev/null +++ b/Zend/tests/type_declarations/relative_types/static/namespace_relative_static.phpt @@ -0,0 +1,12 @@ +--TEST-- +namespace\static is not a valid type declaration +--DESCRIPTION-- +OSS-Fuzz: https://issues.oss-fuzz.com/issues/532353396 +--FILE-- + +--EXPECTF-- +Fatal error: Type declaration 'static' must be unqualified in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d8d61ea979e4..941873663848 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -272,6 +272,7 @@ static const builtin_type_info builtin_types[] = { {ZEND_STRL("iterable"), IS_ITERABLE}, {ZEND_STRL("object"), IS_OBJECT}, {ZEND_STRL("mixed"), IS_MIXED}, + {ZEND_STRL("static"), IS_STATIC}, {NULL, 0, IS_UNDEF} }; @@ -7100,6 +7101,8 @@ static zend_type zend_compile_single_typename(zend_ast *ast) ZSTR_VAL(zend_string_tolower(type_name))); } + ZEND_ASSERT(type_code != IS_STATIC && "unqualified static type should have been handled by ZEND_AST_TYPE branch"); + /* Transform iterable into a type union alias */ if (type_code == IS_ITERABLE) { /* Set iterable bit for BC compat during Reflection and string representation of type */ From 4e64a2655e9444afaf12970c41d45fbbc9abb2c7 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 3 Jul 2026 11:50:34 +0800 Subject: [PATCH 09/16] Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during request activation) Closes GH-22568 --- NEWS | 4 ++++ sapi/cli/php_cli.c | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 46824ac7d6ae..0382330fa8c1 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.27 +- CLI + . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during + request activation). (matyhtf) + - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function registrations are freed while still reachable from the cycle collector. diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 4b19cf0604c6..6894f57c12c0 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -356,6 +356,15 @@ static void sapi_cli_log_message(const char *message, int syslog_type_int) /* {{ } /* }}} */ +static int sapi_cli_activate(void) /* {{{ */ +{ +#if defined(PHP_WIN32) && defined(ZTS) + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + return SUCCESS; +} +/* }}} */ + static int sapi_cli_deactivate(void) /* {{{ */ { fflush(stdout); @@ -420,7 +429,7 @@ static sapi_module_struct cli_sapi_module = { php_cli_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ - NULL, /* activate */ + sapi_cli_activate, /* activate */ sapi_cli_deactivate, /* deactivate */ sapi_cli_ub_write, /* unbuffered write */ From 30920e9004be86f9cabc64e258b63c9806a878f3 Mon Sep 17 00:00:00 2001 From: Manish Kafle Date: Sat, 12 Sep 2026 05:54:42 +0000 Subject: [PATCH 10/16] Fix GH-23662: avoid NAN warning in print_r() --- NEWS | 1 + Zend/tests/gh23662.phpt | 12 ++++++++++++ Zend/zend.c | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 Zend/tests/gh23662.phpt diff --git a/NEWS b/NEWS index 98f99ad217ee..ead9e3308b9a 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ PHP NEWS . Fixed bug GH-23644 (Optimizer leaves a constant-vs-constant comparison unfolded, crashing the VM in zval_undefined_cv). (ndossche) . Fixed OSS-Fuzz 532353396 (assertion failure with static type). (Girgias) + . Fix GH-23662 (Avoid NAN warning in print_r()). (CodedByManish) - CLI . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during diff --git a/Zend/tests/gh23662.phpt b/Zend/tests/gh23662.phpt new file mode 100644 index 000000000000..426b6e2aa5d7 --- /dev/null +++ b/Zend/tests/gh23662.phpt @@ -0,0 +1,12 @@ +--TEST-- +GH-23662 (print_r() results in warning for NAN) +--FILE-- + +--EXPECT-- +NANArray +( + [0] => NAN +) diff --git a/Zend/zend.c b/Zend/zend.c index aa621c4daa8e..43cf8f5ccef2 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -597,6 +597,9 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /* case IS_LONG: smart_str_append_long(buf, Z_LVAL_P(expr)); break; + case IS_DOUBLE: + smart_str_append_double(buf, Z_DVAL_P(expr), EG(precision), false); + break; case IS_REFERENCE: zend_print_zval_r_to_buf(buf, Z_REFVAL_P(expr), indent); break; From c2c01aea6c4ef9bf6fee6b8e460ac749e72f105f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 21 Aug 2026 14:11:26 -0400 Subject: [PATCH 11/16] Fix heap over-read in cli_get_prompt() for empty cli.prompt The prompt parser ran as a do-while, so an empty cli.prompt executed the body on the terminator and scanned past it. Use a while loop and smart_str_extract(), which returns the interned empty string when nothing was appended. No regression test: extra unicode warnings from the over-read depend on heap contents, so a .phpt cannot pin the bug red-before. Closes GH-23415 --- NEWS | 4 ++++ ext/readline/readline_cli.c | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 0382330fa8c1..d81d3170a6f0 100644 --- a/NEWS +++ b/NEWS @@ -38,6 +38,10 @@ PHP NEWS . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- Readline: + . Fixed a heap over-read in the interactive shell prompt when cli.prompt is + set to an empty string. (Ilia Alshanetsky) + - Sockets: . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on Windows. (David Carlier) diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c index ff5caee9eb7d..8319b3b43641 100644 --- a/ext/readline/readline_cli.c +++ b/ext/readline/readline_cli.c @@ -130,7 +130,7 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ char *prompt_spec = CLIR_G(prompt) ? CLIR_G(prompt) : DEFAULT_PROMPT; bool unicode_warned = false; - do { + while (*prompt_spec) { if (*prompt_spec == '\\') { switch (prompt_spec[1]) { case '\\': @@ -198,9 +198,9 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ smart_str_appendc(&retval, '?'); } } - } while (++prompt_spec && *prompt_spec); - smart_str_0(&retval); - return retval.s; + ++prompt_spec; + } + return smart_str_extract(&retval); } /* }}} */ From 8d0d6307aaca39223d5dd9461dc8dea90286a3bb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 21 Aug 2026 13:13:21 -0400 Subject: [PATCH 12/16] Fix three Win32-only defects in proc_open descriptor handling init_process_info() memset the pointer parameter instead of the PROCESS_INFORMATION structure it points at, leaving the struct uninitialized before CreateProcessW(). Zero it through the pointer. find_comspec_nt() dereferences *comspec in its cleanup while the caller only assigns it on success, so a failed SearchPathW() read an indeterminate value. Initialize the caller's variable to NULL. set_proc_descriptor_to_blackhole() tested CreateFileA() against NULL, but CreateFileA() signals failure with INVALID_HANDLE_VALUE, so a failed open went undetected and an invalid handle was inherited by the child. Test against INVALID_HANDLE_VALUE. Closes GH-23412 --- NEWS | 5 +++++ ext/standard/proc_open.c | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index d81d3170a6f0..51580b6c7781 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,11 @@ PHP NEWS . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) +- Standard: + . Fixed three Windows-only proc_open() defects: an uninitialized + PROCESS_INFORMATION, an indeterminate comspec pointer after a failed + lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + - XSL: . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() is called during a transformation). (David Carlier) diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index d2d51de5a856..bd4cf7a0a0ee 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -695,7 +695,7 @@ static void init_startup_info(STARTUPINFOW *si, descriptorspec_item *descriptors static void init_process_info(PROCESS_INFORMATION *pi) { - memset(&pi, 0, sizeof(pi)); + memset(pi, 0, sizeof(*pi)); } /* on success, returns length of *comspec, which then needs to be efree'd by caller */ @@ -746,7 +746,7 @@ static size_t find_comspec_nt(wchar_t **comspec) static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len) { - wchar_t *comspec; + wchar_t *comspec = NULL; size_t len = find_comspec_nt(&comspec); if (len == 0) { php_error_docref(NULL, E_WARNING, "Command conversion failed"); @@ -829,7 +829,7 @@ static zend_result set_proc_descriptor_to_blackhole(descriptorspec_item *desc) #ifdef PHP_WIN32 desc->childend = CreateFileA("nul", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); - if (desc->childend == NULL) { + if (desc->childend == INVALID_HANDLE_VALUE) { php_error_docref(NULL, E_WARNING, "Failed to open nul"); return FAILURE; } From 3213c3faa641265868f0d6aec3c17462382d5809 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:04:37 +0200 Subject: [PATCH 13/16] Fix OSS-Fuzz #546798343: Heap-buffer-overflow in zend_delete_call_instructions with callable conversion Call level counter was broken: ZEND_CALLABLE_CONVERT was forgotten. Closes GH-23777. --- NEWS | 4 ++++ Zend/Optimizer/optimize_func_calls.c | 1 + ext/opcache/tests/opt/oss_fuzz_546798343.phpt | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 ext/opcache/tests/opt/oss_fuzz_546798343.phpt diff --git a/NEWS b/NEWS index 51580b6c7781..2eef981bcf60 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,10 @@ PHP NEWS . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence). (Lazizbek Ergashev) +- Opcache: + . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with + FCCs and inlining). (ndossche) + - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index ce6c43afaedb..5449535c560a 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -63,6 +63,7 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli case ZEND_DO_ICALL: case ZEND_DO_UCALL: case ZEND_DO_FCALL_BY_NAME: + case ZEND_CALLABLE_CONVERT: call++; break; case ZEND_SEND_VAL: diff --git a/ext/opcache/tests/opt/oss_fuzz_546798343.phpt b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt new file mode 100644 index 000000000000..f7f058a11652 --- /dev/null +++ b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt @@ -0,0 +1,19 @@ +--TEST-- +OSS-Fuzz #546798343 (Heap-buffer-overflow in zend_delete_call_instructions with callable conversion) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + +--EXPECT-- +Done From fb76b3d7158647d2c96fdcf2f5a2feb01d04b425 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 21:26:09 -0400 Subject: [PATCH 14/16] ext/zlib: honor preset dictionary for raw inflate with non-default window inflate_init() applies the preset dictionary eagerly for raw streams via inflateSetDictionary(), gated on encoding == PHP_ZLIB_ENCODING_RAW. But encoding is first adjusted by the window size (encoding += 15 - window), so a raw stream with a non-default window no longer equals PHP_ZLIB_ENCODING_RAW and the dictionary is silently dropped; raw streams carry no header and never emit Z_NEED_DICT, so inflate_add()'s deferred path never applies it either. Gate on the pre-adjustment encoding. The deflate side already applies the dictionary unconditionally, so the roundtrip was broken for this case. Closes GH-22381 --- NEWS | 4 ++++ .../tests/inflate_raw_dictionary_window.phpt | 20 +++++++++++++++++++ ext/zlib/zlib.c | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 ext/zlib/tests/inflate_raw_dictionary_window.phpt diff --git a/NEWS b/NEWS index 39010ac77b12..41e9dd506996 100644 --- a/NEWS +++ b/NEWS @@ -62,6 +62,10 @@ PHP NEWS PROCESS_INFORMATION, an indeterminate comspec pointer after a failed lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) +- Zlib: + . Fixed inflate_init() dropping the preset dictionary for raw streams with + a non-default window. (Ilia Alshanetsky) + 24 Sep 2026, PHP 8.5.11 diff --git a/ext/zlib/tests/inflate_raw_dictionary_window.phpt b/ext/zlib/tests/inflate_raw_dictionary_window.phpt new file mode 100644 index 000000000000..0269127c1368 --- /dev/null +++ b/ext/zlib/tests/inflate_raw_dictionary_window.phpt @@ -0,0 +1,20 @@ +--TEST-- +inflate_init(): preset dictionary is honored for raw encoding with a non-default window +--EXTENSIONS-- +zlib +--FILE-- + 10, 'dictionary' => $dict]; + +$def = deflate_init(ZLIB_ENCODING_RAW, $opts); +$comp = deflate_add($def, $data, ZLIB_FINISH); + +$inf = inflate_init(ZLIB_ENCODING_RAW, $opts); +$out = inflate_add($inf, $comp, ZLIB_FINISH); + +var_dump($out === $data); +?> +--EXPECT-- +bool(true) diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c index b221b1f4b55c..003ffa801115 100644 --- a/ext/zlib/zlib.c +++ b/ext/zlib/zlib.c @@ -904,6 +904,7 @@ PHP_FUNCTION(inflate_init) ctx->inflateDictlen = dictlen; ctx->status = Z_OK; + zend_long orig_encoding = encoding; if (encoding < 0) { encoding += 15 - window; } else { @@ -917,7 +918,7 @@ PHP_FUNCTION(inflate_init) RETURN_FALSE; } - if (encoding == PHP_ZLIB_ENCODING_RAW && dictlen > 0) { + if (orig_encoding == PHP_ZLIB_ENCODING_RAW && dictlen > 0) { switch (inflateSetDictionary(&ctx->Z, (Bytef *) ctx->inflateDict, ctx->inflateDictlen)) { case Z_OK: efree(ctx->inflateDict); From 9c2acc343e55dc59bd92cd5acfdadee4787772d7 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 16 Sep 2026 08:35:00 -0400 Subject: [PATCH 15/16] Fix GH-23693: JIT guard branches on stale flags across basic blocks The x86 matcher folds v = BINOP(a, b); c = CMP(v, 0); GUARD(c) into IR_GUARD_JCC_INT, emitting the BINOP, dropping the CMP and branching on the flags the BINOP left, but it only required the BINOP to precede the CMP in the IR. Once GCM hoists a loop-invariant BINOP into a dominating block, the jcc reads flags the intervening code has clobbered and the guard fires on whatever is in EFLAGS. Require the BINOP to sit in the guard's block and allow only snapshots between the comparison and the guard, the way ir_match_fuse_load() pairs ir_in_same_block() with ir_match_has_mem_deps(). The sibling MEM_BINOP fold already checks the block, the IF side folds are pinned by full ref adjacency, and ir_aarch64.dasc has no guard fold. Mirrors the upstream fix dstogov/ir@51107a3. Fixes GH-23693 Closes GH-23711 --- NEWS | 2 ++ ext/opcache/jit/ir/ir_x86.dasc | 19 +++++++++++++- ext/opcache/tests/jit/gh23693.phpt | 41 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/jit/gh23693.phpt diff --git a/NEWS b/NEWS index 2eef981bcf60..982945e5ebfc 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,8 @@ PHP NEWS - Opcache: . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with FCCs and inlining). (ndossche) + . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a + loop-invariant addition). (Ilia Alshanetsky) - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc index ca42001a8816..f5efb66698da 100644 --- a/ext/opcache/jit/ir/ir_x86.dasc +++ b/ext/opcache/jit/ir/ir_x86.dasc @@ -1923,6 +1923,21 @@ static bool ir_match_has_mem_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) return 0; } +/* A naive check if anything that emits code, and so clobbers the flags, is + * scheduled between the flags setting instruction and the fusion root */ +static bool ir_match_has_flags_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) +{ + ir_ref pos = ctx->prev_ref[root]; + + while (pos > ref) { + if (ctx->ir_base[pos].op != IR_SNAPSHOT) { + return 1; + } + pos = ctx->prev_ref[pos]; + } + return pos != ref; +} + static void ir_match_fuse_load(ir_ctx *ctx, ir_ref ref, ir_ref root) { if (ir_in_same_block(ctx, ref) && @@ -3089,7 +3104,9 @@ store_int: if (IR_IS_CONST_REF(op2_insn->op2) && !IR_IS_SYM_CONST(ctx->ir_base[op2_insn->op2].op) && ctx->ir_base[op2_insn->op2].val.i64 == 0) { - if (op2_insn->op1 == insn->op2 - 1) { /* previous instruction */ + if (op2_insn->op1 == insn->op2 - 1 /* previous instruction */ + && ir_in_same_block(ctx, op2_insn->op1) + && !ir_match_has_flags_deps(ctx, insn->op2, ref)) { ir_insn *op1_insn = &ctx->ir_base[op2_insn->op1]; if ((op1_insn->op == IR_OR || op1_insn->op == IR_AND || op1_insn->op == IR_XOR) || diff --git a/ext/opcache/tests/jit/gh23693.phpt b/ext/opcache/tests/jit/gh23693.phpt new file mode 100644 index 000000000000..1540f0c1368f --- /dev/null +++ b/ext/opcache/tests/jit/gh23693.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23693: Tracing JIT reads stale flags for a guard on a hoisted addition +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_func=1 +--EXTENSIONS-- +opcache +--FILE-- +> 2; + $s = 0; + for ($dy = -1; $dy <= 1; ++$dy) { + for ($i = 0; $i < $n; ++$i) { + $ny = $y + $dy; + if ($ny < 0) { + continue; + } + if ($ny >= 4) { + continue; + } + $s += ($ny << 2) | ($i & 3); + } + } + return $s; +} +for ($k = 0; $k < 30; ++$k) { + f(16, 200); +} +var_dump(f(16, 200)); +var_dump(f(0, 200)); +?> +--EXPECT-- +int(2700) +int(1400) From 1b4d9f1e32841ee36e035b003a7a54f375a6a234 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 10:29:39 -0400 Subject: [PATCH 16/16] ext/pdo: Report $options as the argument for constructor option errors An attribute that fails to set from the PDO constructor options array named argument #3 ($password) instead of #4 ($options), because php_pdo_internal_construct_driver() passed a hardcoded value_arg_num of 3 to pdo_dbh_attribute_set(). PDO::setAttribute(), the only other caller, already passes the correct number. --- NEWS | 2 ++ ext/pdo/pdo_dbh.c | 2 +- ext/pdo/tests/pdo_constructor_option_argnum.phpt | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 ext/pdo/tests/pdo_constructor_option_argnum.phpt diff --git a/NEWS b/NEWS index 1c937b7cb017..8ae9fe6b2d59 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,8 @@ PHP NEWS - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) + . Fixed the argument reported in error messages when setting an invalid + attribute via the PDO constructor options array. (Ilia Alshanetsky) - PGSQL: . Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky) diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 43c2cf8c0945..ea22fb388a7a 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -516,7 +516,7 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen ZVAL_DEREF(attr_value); /* TODO: Should the constructor fail when the attribute cannot be set? */ - pdo_dbh_attribute_set(dbh, long_key, attr_value, 3); + pdo_dbh_attribute_set(dbh, long_key, attr_value, 4); } ZEND_HASH_FOREACH_END(); } diff --git a/ext/pdo/tests/pdo_constructor_option_argnum.phpt b/ext/pdo/tests/pdo_constructor_option_argnum.phpt new file mode 100644 index 000000000000..642e9bf182aa --- /dev/null +++ b/ext/pdo/tests/pdo_constructor_option_argnum.phpt @@ -0,0 +1,15 @@ +--TEST-- +PDO constructor option attribute errors report $options as argument #4 +--EXTENSIONS-- +pdo +pdo_sqlite +--FILE-- + 999]); +} catch (Throwable $e) { + echo get_class($e), ": ", str_replace(PHP_VERSION, '', $e->getMessage()), "\n"; +} +?> +--EXPECT-- +ValueError: PDO::__construct(): Argument #4 ($options) Error mode must be one of the PDO::ERRMODE_* constants