From c135a869c1404a95d0ef0d36bfd3a0342cdc46aa Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 09:47:39 -0400 Subject: [PATCH 1/4] ext/pgsql: Restore binary-safe pg_lo_write() The conversion of the data parameter to Z_PARAM_PATH_STR() made pg_lo_write() reject any data containing a null byte, breaking writes of arbitrary binary payloads which were accepted before. Accept embedded null bytes again by parsing the data as a length-aware string. Sibling audit: the other Z_PARAM_PATH* conversions from the same change (pg_connect(), pg_lo_import(), table/statement identifiers) operate on paths or SQL text where null-byte rejection is intentional. Closes GH-23649 --- NEWS | 3 ++ ext/pgsql/pgsql.c | 2 +- ext/pgsql/tests/05large_object.phpt | 6 --- ext/pgsql/tests/pg_lo_write_null_bytes.phpt | 49 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 ext/pgsql/tests/pg_lo_write_null_bytes.phpt diff --git a/NEWS b/NEWS index 60c8b43a3257..9642ef64b099 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,9 @@ PHP NEWS . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence). (Lazizbek Ergashev) +- PGSQL: + . Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky) + 24 Sep 2026, PHP 8.5.11 diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 0106b074fd4f..16e8589219b8 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -2808,7 +2808,7 @@ PHP_FUNCTION(pg_lo_write) ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_OBJECT_OF_CLASS(pgsql_id, pgsql_lob_ce) - Z_PARAM_PATH_STR(str) + Z_PARAM_STR(str) Z_PARAM_OPTIONAL Z_PARAM_LONG_OR_NULL(z_len, z_len_is_null) ZEND_PARSE_PARAMETERS_END(); diff --git a/ext/pgsql/tests/05large_object.phpt b/ext/pgsql/tests/05large_object.phpt index 957f2dffa8ea..a785b76a572d 100644 --- a/ext/pgsql/tests/05large_object.phpt +++ b/ext/pgsql/tests/05large_object.phpt @@ -17,11 +17,6 @@ $oid = pg_lo_create ($db); if (!$oid) echo ("pg_lo_create() error\n"); $handle = pg_lo_open ($db, $oid, "w"); if (!$handle) echo ("pg_lo_open() error\n"); -try { - pg_lo_write ($handle, "large\0object data"); -} catch (\ValueError $e) { - echo $e->getMessage(), PHP_EOL; -} pg_lo_write ($handle, "large object data"); pg_lo_close ($handle); pg_exec ($db, "COMMIT"); @@ -110,7 +105,6 @@ echo "OK"; ?> --EXPECTF-- create/write/close LO -pg_lo_write(): Argument #2 ($data) must not contain any null bytes open/read/tell/seek/close LO string(5) "large" int(5) diff --git a/ext/pgsql/tests/pg_lo_write_null_bytes.phpt b/ext/pgsql/tests/pg_lo_write_null_bytes.phpt new file mode 100644 index 000000000000..44073692a182 --- /dev/null +++ b/ext/pgsql/tests/pg_lo_write_null_bytes.phpt @@ -0,0 +1,49 @@ +--TEST-- +pg_lo_write() must accept data containing null bytes +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +try { + pg_lo_write($handle, "abc", 4); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} +pg_lo_close($handle); +pg_exec($db, "ROLLBACK"); + +pg_exec($db, "BEGIN"); +$oid = pg_lo_create($db); +$handle = pg_lo_open($db, $oid, "w"); +pg_lo_write($handle, "x\0y"); +pg_lo_close($handle); +$handle = pg_lo_open($db, $oid, "r"); +var_dump(bin2hex(pg_lo_read($handle))); +pg_lo_close($handle); +pg_exec($db, "ROLLBACK"); +?> +--EXPECT-- +int(12) +int(4) +int(2) +int(0) +ValueError: pg_lo_write(): Argument #3 ($length) must be greater than or equal to 0 +ValueError: pg_lo_write(): Argument #3 ($length) must be less than or equal to the length of argument #2 ($buf) +string(6) "780079" From 2311816c9ef728f0dae8539de27be5bfea678e9c Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 16 Aug 2026 12:59:01 -0400 Subject: [PATCH 2/4] ext/intl: Resolve UConverter callbacks when the object is allocated __construct filled to_cache/from_cache via php_converter_resolve_callback, so a clone, which never runs the constructor, kept both FCCs zeroed and the first callback-worthy convert called zend_call_known_fcc on a NULL handler. Resolve both caches in php_converter_object_ctor so every allocation path gets them. Closes GH-23324 --- NEWS | 2 ++ ext/intl/converter/converter.c | 4 ++-- ext/intl/tests/uconverter_clone_callback.phpt | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 ext/intl/tests/uconverter_clone_callback.phpt diff --git a/NEWS b/NEWS index 9642ef64b099..511a899e527a 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,8 @@ PHP NEWS . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state such as dateType, timeType, calendar and the message pattern. (Ilia Alshanetsky) + . Fixed a crash when converting with a cloned UConverter that uses + toUCallback/fromUCallback. (Ilia Alshanetsky) - MBString: . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in diff --git a/ext/intl/converter/converter.c b/ext/intl/converter/converter.c index 759db5e18873..bf89691dd6ea 100644 --- a/ext/intl/converter/converter.c +++ b/ext/intl/converter/converter.c @@ -546,8 +546,6 @@ PHP_METHOD(UConverter, __construct) { ZEND_ASSERT(EG(exception)); goto cleanup; } - php_converter_resolve_callback(&objval->to_cache, Z_OBJ_P(ZEND_THIS), ZEND_STRL("toUCallback")); - php_converter_resolve_callback(&objval->from_cache, Z_OBJ_P(ZEND_THIS), ZEND_STRL("fromUCallback")); cleanup: INTL_G(use_exceptions) = old_use_exception; INTL_G(error_level) = old_error_level; @@ -916,6 +914,8 @@ static zend_object *php_converter_object_ctor(zend_class_entry *ce, php_converte zend_object_std_init(&objval->obj, ce); object_properties_init(&objval->obj, ce); intl_error_init(&(objval->error)); + php_converter_resolve_callback(&objval->to_cache, &objval->obj, ZEND_STRL("toUCallback")); + php_converter_resolve_callback(&objval->from_cache, &objval->obj, ZEND_STRL("fromUCallback")); *pobjval = objval; diff --git a/ext/intl/tests/uconverter_clone_callback.phpt b/ext/intl/tests/uconverter_clone_callback.phpt new file mode 100644 index 000000000000..140841b455e5 --- /dev/null +++ b/ext/intl/tests/uconverter_clone_callback.phpt @@ -0,0 +1,24 @@ +--TEST-- +Cloned UConverter resolves toUCallback/fromUCallback on the clone +--EXTENSIONS-- +intl +--FILE-- +hits++; + return parent::toUCallback($reason, $source, $codeUnits, $error); + } +} + +$orig = new MyConverter('ascii', 'utf-8'); +$clone = clone $orig; +$clone->convert("irregul\xC1\xA1r"); +echo $clone->hits > 0 ? "ok\n" : "no callback\n"; + +?> +--EXPECT-- +ok From 2101993133e849f3e84da9c478126cc7cb791996 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 14 Sep 2026 00:03:40 +0800 Subject: [PATCH 3/4] ext/standard: Optimize octal escape generation in addcslashes() (#23680) This hack is slightly faster than the original one. My local benchmarks show approximately 39% lower runtime for a single NUL byte. --- UPGRADING | 1 + ext/standard/string.c | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/UPGRADING b/UPGRADING index 7c7b727759b6..07223cd7a21f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1098,6 +1098,7 @@ PHP 8.6 UPGRADE NOTES . Reduced temporary allocations when iterating Phar directories. - Standard: + . Improved performance of addcslashes() when generating octal escapes. . Improved performance of sorting single-element arrays. . Improved performance of array_fill_keys(). . Improved performance of array_intersect(). diff --git a/ext/standard/string.c b/ext/standard/string.c index 577d4267ed2f..f7c2ed37cf64 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -3904,7 +3904,11 @@ PHPAPI zend_string *php_addcslashes_str(const char *str, size_t len, const char case '\v': *target++ = 'v'; break; case '\b': *target++ = 'b'; break; case '\f': *target++ = 'f'; break; - default: target += snprintf(target, 4, "%03o", (unsigned char) c); + default: + /* Write the byte as three octal digits, including leading zeros. */ + *target++ = ((unsigned char) c >> 6) + '0'; + *target++ = (((unsigned char) c >> 3) & 7) + '0'; + *target++ = ((unsigned char) c & 7) + '0'; } continue; } From c36758eefbc681c2e3c4f64c346b48c1edac85c3 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Sun, 13 Sep 2026 17:59:47 +0000 Subject: [PATCH 4/4] Avoid closing libxml output encoder twice (#23678) --- ...t_saveHTMLFile_failed_output_encoding.phpt | 31 +++++++++++++++++++ ext/libxml/libxml.c | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/DOMDocument_saveHTMLFile_failed_output_encoding.phpt diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_failed_output_encoding.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_failed_output_encoding.phpt new file mode 100644 index 000000000000..05adefb973aa --- /dev/null +++ b/ext/dom/tests/DOMDocument_saveHTMLFile_failed_output_encoding.phpt @@ -0,0 +1,31 @@ +--TEST-- +DOMDocument::saveHTMLFile() does not close the encoder twice when opening the output fails +--EXTENSIONS-- +dom +--FILE-- +loadHTML('value'); + $doc->getElementsByTagName('meta')->item(0)->setAttribute('content', 'text/html; charset=' . $encoding); + for ($i = 0; $i < 3; $i++) { + $result = @$doc->saveHTMLFile($filename); + var_dump($result === 0 || $result === false); + } + var_dump($doc->getElementsByTagName('body')->item(0)->textContent === 'value'); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index fa9432444366..c73bcf930cfd 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -618,8 +618,10 @@ php_libxml_output_buffer_create_filename(const char *URI, return(ret); err: - /* Similarly to __xmlOutputBufferCreateFilename we should also close the encoder on failure. */ +#if LIBXML_VERSION < 21404 + /* As of libxml 2.14.4, libxml closes the encoder after this callback fails. */ xmlCharEncCloseFunc(encoder); +#endif return NULL; }