diff --git a/.github/matrix.php b/.github/matrix.php index ee9cb4eb05ab..87486791cff5 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -1,7 +1,8 @@ 'master', 'ref' => 'master', 'version' => [8, 6]], + ['name' => 'master', 'ref' => 'master', 'version' => [8, 7]], + ['name' => 'PHP-8.6', 'ref' => 'PHP-8.6', 'version' => [8, 6]], ['name' => 'PHP-8.5', 'ref' => 'PHP-8.5', 'version' => [8, 5]], ['name' => 'PHP-8.4', 'ref' => 'PHP-8.4', 'version' => [8, 4]], ['name' => 'PHP-8.3', 'ref' => 'PHP-8.3', 'version' => [8, 3]], diff --git a/.github/scripts/windows/find-target-branch.bat b/.github/scripts/windows/find-target-branch.bat index 44b0bde1ec8c..1fd05a720ef7 100644 --- a/.github/scripts/windows/find-target-branch.bat +++ b/.github/scripts/windows/find-target-branch.bat @@ -3,6 +3,6 @@ for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i -if /i "%BRANCH%" equ "8.6" ( +if /i "%BRANCH%" equ "8.7" ( set BRANCH=master ) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eafedec5eafa..057ed83eb226 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ on: - PHP-8.3 - PHP-8.4 - PHP-8.5 + - PHP-8.6 - master pull_request: paths-ignore: *ignore_paths diff --git a/NEWS b/NEWS index c91c7ce877af..01eb73754ac1 100644 --- a/NEWS +++ b/NEWS @@ -1,1128 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.6.0RC1 - -- Core: - . Fixed incorrect internal pointer and foreach iterator positions when - compacting arrays with holes. (Weilin Du) - . Fix handling of references to typed properties during unserialization - of various internal classes. (ndossche, timwolla) - . Fixed OSS-Fuzz 532353396 (assertion failure with static type). (Girgias) - . Fix GH-23662 (Avoid NAN warning in print_r()). (CodedByManish) - . Fixed bug GH-23752 (Use scoped diagnostic suppression for the global - register declarations so the caller's -Wvolatile-register-var state is - restored). (yqtian-se) - . Fixed OSS-Fuzz #538730793 (Assertion failure when returning by-ref from - closure invoke). (ndossche) - . Fixed OSS-Fuzz #540904105 (ASSERT: ast->attr == T_CLASS_C). (ndossche) - -- CLI - . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during - request activation). (matyhtf) - -- Date: - . Fix unserialization of Time\Duration. (timwolla) - . Add comparison handler for Time\Duration. (timwolla) - -- DOM: - . Fixed use-after-free when re-constructing a DOMXPath whose php:function - registrations are freed while still reachable from the cycle collector. - (Ilia Alshanetsky) - . 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) - -- Fileinfo: - . Upgrade to file 5.48. (Weilin Du) - -- Intl: - . 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) - -- Lexbor: - . Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a - heap buffer overflow in :lexbor-contains() parsing and buffer overflows - in malformed decode replay. (alexandre-daubois) - -- MBString: - . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in - a truncated UTF-8 sequence). (Lazizbek Ergashev) - . Updated Unicode data tables to Unicode 18.0 (Yuya Hamada) - -- MySQLi: - . Fix GH-22854: Fixed failed assertion when accessing mysqli property after - failed reconnection. (Kamil Tekiela) - -- Opcache: - . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a - loop-invariant addition). (Ilia Alshanetsky) - . Fixed OSS-Fuzz #545352966 (default value AST of an SHM-persisted partial). - (ndossche) - -- PDO: - . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid - column index. (Ilia Alshanetsky) - . Fixed PDOStatement::bindColumn() registering a binding for a column name - that is not in the result set. (Ilia Alshanetsky) - -- PGSQL: - . Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky) - -- Posix: - . Reverted the validity check on the flags argument of posix_access(). - (David Carlier) - -- Readline: - . Fixed a heap over-read in the interactive shell prompt when cli.prompt is - set to an empty string. (Ilia Alshanetsky) - -- SPL: - . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when - __serialize() removes an element). (David Carlier) - -- SQLite: - . Fixed a crash when SQLite3::close() is called from a userland callback. - (Ilia Alshanetsky) - -- Standard: - . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false - properly). (Jakub Zelenka, Ilija Tovilo) - . 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) - -- Zlib: - . Fixed inflate_init() dropping the preset dictionary for raw streams with - a non-default window. (Ilia Alshanetsky) - - -10 Sep 2026, PHP 8.6.0beta3 - -- BCMath: - . Fixed out-of-bounds read in bc_is_zero_for_scale() when scale exceeds - n_scale. (Ilia Alshanetsky) - -- Core: - . Fixed out-of-bounds reads during automatic UTF-16/32 encoding detection. - (Yudai Takada) - . Calling is_a() or is_subclass_of() with a string as the first argument - when $allow_string is false is now deprecated. (Daniel Scherzer) - . Fixed bug GH-23232 (lone namespace separator asks the autoloader for an - empty class name). (spawnia) - -- CLI: - . Fixed bug GH-23242 (PHP development server does not support Expect - 100-continue flow control). (Sjoerd Langkemper) - -- Calendar: - . Fixed *tojd() functions and cal_to_jd() truncating arguments outside the - int range instead of rejecting them. (lacatoire) - -- DOM: - . Fixed NamedNodeMap::getNamedItemNS() with an empty URI not matching - the null namespace in spec-following mode. (Ilia Alshanetsky) - . Fixed stale getElementsByClassName() and other node list caches after - className/classList writes and attribute removals. (Ilia Alshanetsky) - . Fixed reference cycles through DOMXPath and XSLTProcessor php:function - callback arguments and return values not being collectable. - (Ilia Alshanetsky) - -- Hash: - . Fixed hash_file() reporting argument #1 ($algo) instead of argument #2 - ($filename) when the filename contains null bytes. (lacatoire) - -- Intl: - . Fixed grapheme_strpos() and grapheme_strrpos() with an empty needle - returning UTF-16 offsets instead of grapheme offsets. (Ilia Alshanetsky) - . Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky) - . Fixed Collator::sortWithSortKeys() allocating fixed 2MiB buffers - regardless of array size. (Ilia Alshanetsky) - . Fixed a memory leak when iterating IntlBreakIterator::getPartsIterator() - results. (iliaal) - . Fixed a leak in Locale::getKeywords() when a keyword value cannot be - read. (iliaal) - . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed - from compiled rules. (iliaal) - . Fixed Spoofchecker methods not recording the ICU error code when an ICU - call fails. (Ilia Alshanetsky) - . Fixed idn_to_ascii() and idn_to_utf8() reporting argument #2 ($flags) - instead of argument #3 ($variant) for an invalid IDNA variant, and the - domain length error message printing a literal "d" instead of the limit. - (lacatoire) - -- MBString: - . Fixed mb_ereg_replace() emitting a NUL or out-of-bounds bytes in the - replacement when a \k backref has no closing delimiter. - (Ilia Alshanetsky) - -- ODBC: - . Fixed odbc_field_len(), odbc_field_scale() and odbc_field_type() - returning uninitialized memory when SQLColAttribute fails. - (Ilia Alshanetsky) - . odbc_error() and odbc_errormsg() now also report SQLColAttribute - failures. (Ilia Alshanetsky) - -- PCNTL: - . Fixed the declared signature of pcntl_signal(), whose $restart_syscalls - argument accepts null and defaults to it. (lacatoire) - -- PDO_PGSQL: - . Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the - given number of rows. (KentarouTakeda) - -- PGSQL: - . Fixed the pg_insert(), pg_update() and pg_delete() flag error messages, - which did not name the set of flags actually accepted. (lacatoire) - -- Phar: - . Fixed bug GH-23418 (Use-after-free when looking up mounted directories). - (Weilin Du) - . Fixed bug GH-23477 (Memory leak on duplicate native Phar manifest entries). - (Weilin Du) - -- Sockets: - . Fixed socket_set_option() with SO_ATTACH_REUSEPORT_CBPF and a zero value, - which detached the classic BPF filter instead of the reuseport program. - (David Carlier) - -- SOAP: - . Fixed WSDL cache corruption when a soap:header defines headerfaults. - (Ilia Alshanetsky) - . Fixed stack overflow when parsing a WSDL with self-referential schema - groups or attributeGroups. (Ilia Alshanetsky) - -- Sodium: - . Added support for the libsodium 1.0.22 KEM APIs (X-Wing and ML-KEM768). - (Zachary DuBois) - -- Standard: - . Fixed a segfault when a stream filter callback unsets StreamBucket::$data - before re-attaching the bucket. (iliaal) - . Fixed an out-of-bounds read when following a redirect response with an - empty Location header. (iliaal) - . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm) - . Io\Poll\Context::wait() now rejects a $maxEvents value greater than - INT_MAX instead of truncating it. (marc-mabe) - . Fixed GH-23338 (fsockopen()/pfsockopen() ValueError reported wrong - argument number for $timeout). (lacatoire) - . Fixed bug GH-23576 (Next index for array returned from array_keys() is - wrong). (Lazizbek Ergashev) - -- SimpleXML: - . Fixed writing to a dimension of the object returned by attributes() not - creating the attribute. (Ilia Alshanetsky) - . Fixed child elements of the element returned by - SimpleXMLElement::addChild() not being accessible by property name when - namespaces are involved. (Ilia Alshanetsky) - -- Streams: - . Added so_rcvbuf and so_sndbuf stream socket context options, setting the - socket receive and send buffer sizes in bytes. (David Carlier) - - -27 Aug 2026, PHP 8.6.0beta2 - -- Core: - . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or - next() call on the inner generator). (iliaal) - . Fixed GH-10497 (Allow direct mutation of objects stored in constants or - class constants via OBJ->prop = $val). (Khaled Alam) - . Reverted GH-22833, which attempted to fix bug GH-18985. (ilutov) - . Using the return statement in a finally block is now deprecated. - (aldemeery) - -- Curl: - . Set content length using CURLOPT_POSTFIELDSIZE_LARGE instead of - CURLOPT_POSTFIELDSIZE. This makes it possible to post strings larger than - 2GB on some platforms, e.g. Windows. (Sjoerd Langkemper) - -- DOM: - . Fixed a typo in the DOMException message for INUSE_ATTRIBUTE_ERR. - (Weilin Du) - . Fixed bug GH-22624 (use-after-free via DOMNameSpaceNode after - DOMDocument::xinclude()). (David Carlier) - . Fixed a use-after-free when cloning a DOMNameSpaceNode after - DOMDocument::xinclude(). (iliaal) - . Fixed a crash in DOMXPath when a php:function callback receives a nodeset - and a later callback returns a node from another document. (iliaal) - . Fixed bug GH-23331 (UAF when node_list_unlink() skips attribute children - that still have a live wrapper). (iliaal) - . Fixed a use-after-free when Dom\Element::setAttributeNS() replaces the - value of an attribute whose child still has a live wrapper. (iliaal) - -- GD: - . Fixed imageaffinematrixget() and imageaffinematrixconcat() reporting the - wrong argument in error messages. (Weilin Du) - . Fixed imageaffinematrixget() to enforce the documented array|float type - for the $options parameter. (Weilin Du) - -- Intl: - . Fixed grapheme_strrev() treating UBRK_DONE as a byte index and leaving - the result without a terminating NUL. (iliaal) - . Fixed a double-free when IntlGregorianCalendar construction fails after - the ICU constructor adopts the TimeZone. (iliaal) - . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions - for UTF-8 strings). (ColumbusLabs) - . Fixed Locale::parseLocale() reading past a trailing '-' or '_'. - (iliaal, Xuyang Zhang) - . Fixed grapheme_str_split() treating UBRK_DONE as a byte index. (iliaal) - -- Opcache: - . Fixed a tracing JIT crash when compiling a side trace for a method of a - class that could not be stored in the inheritance cache. (GH-21710) - (Arnaud, iliaal) - -- PDO: - . Fixed a leak when a persistent connection failed a liveness check - with no other live PDO handle. (iliaal) - -- PDO_PGSQL: - . Fixed several lazy fetch (PDO::ATTR_PREFETCH => 0) defects: an infinite - loop when cleaning up a fetch left in a COPY, a use-after-free when a - statement with emulated or disabled prepares is destroyed, a connection - left busy for the next fetch, and rows delivered from a result another - statement took over. (KentarouTakeda) - -- PGSQL: - . Fixed the class name casing of pg_close_stmt()'s connection parameter. - (lacatoire) - -- Phar: - . Fixed Phar archives being automatically detected when ".phar" only occurs - in a directory name or is not a filename extension in an included file's - path. (Weilin Du) - -- Readline: - . Fixed class constant completion in the interactive shell. (Weilin Du) - -- Zip: - . Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be - garbage collected). (Weilin Du, ndossche) - -- SAPI: - . Fixed fuzzer targets failing to build in isolation. (Mrmaxmeier) - . Fixed returns uninitialized value on LiteSpeed lsapi SAPI (Go Kudo) - -- Session: - . Fixed bug GH-23056 (missing handler name in session write warning). - (lazerg) - . SessionHandler::validateId() is now implemented, so - session.use_strict_mode applies to the built-in handler. (Girgias) - . It is now deprecated to pass an object that does not implement the - create_sid() and validateId() methods. (Girgias) - . A deprecation is now emitted when implementing SessionHandlerInterface on a - class which doesn't define the create_sid() or validateId() methods, as - those will be moved from SessionUpdateTimestampHandlerInterface and - SessionIdInterface to SessionHandlerInterface. (Girgias) - -- SNMP: - . It is now possible to use the AES192, AES192C, AES256, and AES256C as - SNMPv3 security protocols if the underlying library supports them. - (eskyuu) - . It is now possible to reset the MIB tree using the new snmp_init_mib() - function. (eskyuu) - . Additional MIB parsing and output control functionality has been exposed - via the snmp_set_mib_option(), snmp_set_output_option(), - snmp_set_string_output_format() functions, the $numeric_index, - $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, - $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), - SNMP::setStringOutputFormat() methods. (eskyuu) - -- Sodium: - . Fixed incorrect parameter name in sodium_add(), sodium_memcmp(), and - sodium_compare() length-mismatch error messages. (lacatoire) - -- SPL: - . The following SplFileObject methods have been deprecated - SplFileObject::fgetcsv(), SplFileObject::fputcsv(), - SplFileObject::setCsvControl(), and SplFileObject::getCsvControl(). - (Girgias) - -- Standard: - . Fixed incorrect parameter name in convert_uudecode() warning. (lacatoire) - . Added support for the "<" and ">" endianness modifiers in pack() and - unpack() format codes. (alexandre-daubois) - -- Zip: - . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive - is freed while the stream is still open). (Eyüp Can Akman) - -- Zlib: - . Fixed bug GH-22142 (Assertion failure in deflate_init() when an option - object has uninitialised typed properties). (David Carlier) - -13 Aug 2026, PHP 8.6.0beta1 - -- Core: - . Deprecated "namespace" as a class constant name. (NickSdot) - . Changed run-tests.php to run in parallel by default, using up to 10 - automatically detected workers. Pass -j1 for sequential execution. - (NickSdot) - . Allowed readonly properties to declare default values. (NickSdot) - . Changed run-tests.php to run test subprocesses without a shell where - possible. (NickSdot) - . Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with - -d error_include_args=On). (David Carlier) - . Fixed GH-23121 (is_callable() wrongly accepts objects with no get_closure - handler). (David Carlier) - . Passing a 3rd argument to define() is now deprecated. (Girgias) - . Naming a function readonly is now deprecated. (Girgias) - . Added stateless closure cache. (ilutov) - -- BZ2: - . Passing an object for the Bzip2 {de}compression stream filter is now - deprecated. Use get_object_vars() on the object instead. (Girgias) - -- Curl: - . Improved cURL option validation errors to include the option name. - (Sjoerd Langkemper) - . Raise a value error when the callback registered with CURLOPT_READFUNCTION - returns an unexpected long. (Sjoerd Langkemper) - . Fix bug GH-16513 (curl: exceptions in callbacks do not abort the request). - (Sjoerd Langkemper) - -- Date: - . Update timelib to 2026.02. (Derick, timwolla) - . Added Time\Duration. (timwolla, Derick) - -- DOM: - . Fixed bug GH-23116 (Stack overflow when normalizing a deeply nested - DOMDocument). (Lazizbek Ergashev) - . Fixed bug GH-23117 (Stack overflow when normalizing a deeply nested - Dom\XMLDocument). (Lazizbek Ergashev) - -- Exif: - . Fixed exif_read_data() allocating a HEIF meta box larger than the file - it came from. (iliaal) - -- GMP: - . Added optional $definitely_prime output parameter to gmp_prevprime(). - (Weilin Du) - . Added gmp_powm_sec(). (Weilin Du) - -- Intl: - . Added static methods IntlDatePatternGenerator::getSkeleton() and - IntlDatePatternGenerator::getBaseSkeleton(). (Weilin Du) - . Fixed Collator::sort(), collator_sort(), Collator::asort(), and - collator_asort() to report UTF-8/UTF-16 conversion errors through the intl - error handler instead of emitting a warning and continuing with an empty - string. (Weilin Du) - . Fixed IntlListFormatter::__construct() leaving stale global error state - after successful calls. (Weilin Du) - . Fixed IntlNumberRangeFormatter leaving stale global error state after - successful createFromSkeleton() and format() calls. (Weilin Du) - . Implemented GH-20255 (Add a predefined calendar constant in - IntlDateFormatter for the proleptic gregorian calendar). (David Carlier) - . Added SpoofChecker::areBidiConfusable(). (David Carlier) - . Added SpoofChecker::getBidiSkeleton(). (Weilin Du) - . Added SpoofChecker::getSkeleton(). (David Carlier) - . Fixed IntlNumberRangeFormatter::format() crash when the formatting fails. - (David Carlier) - -- MbString: - . Passing objects to mb_convert_variables() is now deprecated. (Girgias) - -- MySQLi: - . The mysqli_get_charset() function and mysqli::get_charset() method are now deprecated. (Kamil Tekiela) - . The mysqli_stmt_init() function and mysqli::stmt_init() method are now deprecated. (Kamil Tekiela) - . Instantiation of mysqli_stmt without providing the $query parameter is now deprecated. (Kamil Tekiela) - -- PDO: - . Fixed pdo_raise_impl_error() emitting a warning under ERRMODE_SILENT. - (iliaal) - -- PDO_ODBC: - . Fixed bug GH-23016 (NULL values in long columns come back as garbage - binary strings). (Calvin Buckley, iliaal) - -- Readline: - . Fixed the interactive shell not waiting for the pager process to exit. - (Weilin Du) - -- Reflection: - . Added ReflectionAttribute::inNamespace(), - ReflectionAttribute::getNamespaceName(), and - ReflectionAttribute::getShortName(). (Girgias) - . Fixed bug GH-22905 (Reflection exception messages truncate on null bytes). - (DanielEScherzer) - . Fixed ReflectionProperty::isLazy() and skipLazyInitialization() using the - parent slot when a child class hooks an inherited property. (iliaal) - . Fixed segfault in ReflectionMethod::createFromMethodName() on an - uninstantiable subclass. (iliaal) - -- SimpleXML: - . Fixed integer element offsets that cannot resolve aliasing an existing - element. (iliaal) - . SimpleXMLElement::__construct() now raises a ValueError when the $data - argument contains NUL bytes. (iliaal) - . Fixed segfault when comparing uninitialized SimpleXMLElement - instances. (iliaal) - -- SPL: - . The spl_classes() function is now deprecated, use - ReflectionExtension::getClassNames() instead. (Girgias) - . The spl_object_hash() function is now deprecated, use spl_object_id() - instead. (Girgias) - . The following ArrayIterator methods are now deprecated: - * ArrayIterator::getFlags() - * ArrayIterator::setFlags() - * ArrayIterator::asort() - * ArrayIterator::ksort() - * ArrayIterator::uasort() - * ArrayIterator::uksort() - * ArrayIterator::natsort() - * ArrayIterator::natcasesort() - * ArrayIterator::unserialize() - * ArrayIterator::serialize() - (Girgias) - -- Standard: - . Passing an object as the $data argument to http_build_query() is now - deprecated. The interpretation of object values within $data as arrays - is also deprecated. Convert objects to arrays with get_object_vars() - before calling the function. (Girgias) - . Added the "filter.max_filter_count" stream context option for php://filter - URLs. Using more than 16 filters without configuring this option is now - deprecated. (Sjoerd Langkemper) - . The metaphone() function is now deprecated, use a userland phonetic - matching library instead. (Weilin Du) - . Improved performance of array_intersect(). (mehmetcansahin) - . Fixed bug GH-23006 (phpcredits() full-page HTML title says phpinfo()). - (Weilin Du) - . The following functions now raise a ValueError when the $filename argument - contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(), - filegroup(), fileatime(), filemtime(), filectime(), filetype(), - is_writable(), is_readable(), is_executable(), is_file(), is_dir(), - is_link(), file_exists(), lstat(), stat(). (Girgias) - . Fixed bug GH-22818 (stream_filter_register() orphaned user_filter_map on - shutdown re-registration). (David Carlier) - . Io\Poll\Context::wait() now takes a Time\Duration object as a timeout. - (timwolla) - . Passing an object to array_walk{_recursive} is now deprecated. Use - get_object_vars() on the object instead. (Girgias) - . The is_double() function is now deprecated, use is_float() instead. - (Girgias) - . The is_long() and is_integer() functions are now deprecated, use is_int() - instead. (Girgias) - . The doubleval() function is now deprecated, use floatval() instead. - (Girgias) - . The strcoll() function is now deprecated, use Collator::compare() instead. - (Girgias) - . The SORT_LOCALE_STRING constant for the family of sort functions is now - deprecated, use one of the following functions instead: - * Collator::asort() - * Collator::sort() - * Collator::sortWithSortKeys() - (Girgias) - -- Streams: - . Fixed file_put_contents() LOCK_EX early return leaking stream error - operation depth. (iliaal) - -- XSL: - . Fixed use-after-free when a DOMDocument subclass __clone() retains the - stylesheet copy made by XSLTProcessor::importStylesheet(). (iliaal) - -- Zlib - . Passing an object for the zlib deflate and inflate stream filter is now - deprecated. Use get_object_vars() on the object instead. (Girgias) - . Passing an object to the $option argument to deflate_init and inflate_init - is now deprecated. Use get_object_vars() on the object instead. (Girgias) - -30 Jul 2026, PHP 8.6.0alpha3 - -- Core: - . Implemented partial function application RFC. (Arnaud) - . Fixed bug GH-22263 (reset typed property default on every unserialize - failure path). (David Carlier) - . Fixed bug GH-18985 (Wrong line numbers for match with constant arms). - (ilutov) - . Fixed bug GH-18847 (SEGV in zend_fetch_debug_backtrace() when the memory - limit is reached while the tracing JIT enters a call frame). (Arnaud, - iliaal) - -- DOM: - . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD - declares a default value for the attribute). (iliaal) - . Fixed bug GH-23120 (Stack overflow when comparing deeply nested DOM nodes - with DOMNode::isEqualNode()). (Weilin Du) - -- Embed: - . Made php-cli functionality available in embed builds. (henderkes) - -- GMP: - . Added gmp_prevprime(). (Weilin Du, David Carlier) - . Fixed GMP power and shift operators to reject GMP right operands outside - the unsigned long range instead of silently truncating them. (Weilin Du) - . Fixed GMP integer string parsing to reject strings containing NUL bytes - instead of silently truncating them. (Weilin Du) - . Fixed GMP error messages that referenced outdated parameter names. - (Weilin Du) - -- Intl: - . Fixed grammatical issues in Normalizer invalid form and IntlCalendar time - zone offset error messages. (Weilin Du) - . Removed the dependency on the ICU IO library. (Weilin Du) - -- ODBC: - . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the - driver-reported display size). (iliaal) - -- Opcache: - . Re-enable JIT for ZTS builds on Apple Silicon. (realFlowControl) - -- PDO_ODBC: - . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the - driver-reported display size). (iliaal) - . Fixed bug GH-22666 (Heap buffer overflow when an output parameter value is - longer than the declared maxlen). (iliaal) - . Fixed bug GH-22665 (Out-of-bounds write when the ODBC driver reports a - diagnostic message length beyond the error buffer). (iliaal) - -- Phar: - . Fixed grammatical issues and outdated terminology in Phar error messages. - (Weilin Du) - -- Reflection: - . Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes). - (DanielEScherzer) - -- SOAP: - . Fixed header injection through the Content-Type context option, the - soapaction and the cookie names and values. (David Carlier) - . Fixed the SoapClient and SoapServer "classmap" option to reject arrays - containing integer keys, and made SoapClient throw TypeError/ValueError - for invalid "classmap" options. (Weilin Du, David Carlier) - -- MBString: - . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative - offset in a non-UTF-8 encoding). (Eyüp Can Akman) - . Fixed bug GH-21036 (mb_ereg_search_getregs() crashes after mb_eregi() - invalidates the regex cache). (Matthias Goergens) - -- PCRE: - . Fixed bug GH-21134 (Crash with \C + UTF-8). Using \C in UTF-8 patterns is - now forbidden. (Arnaud) - -- Sockets: - . Fixed socket_set_option() validation error messages for UDP_SEGMENT and - TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du) - . Fixed various memory related issues in ext/sockets. (David Carlier) - -- Standard: - . Fixed setlocale() to reject locale names containing NUL bytes instead of - silently truncating them, and to reject arrays passed after the $locales - argument or additional arguments passed after an array $locales argument. - (Weilin Du) - -- Streams: - . Added a new IO copy API used by php_stream_copy_to_stream_ex() that - leverages platform primitives (sendfile, splice, copy_file_range, - TransmitFile) for faster stream copying. (Jakub Zelenka, David Carlier) - . Fixed bug GH-22841 (php_stream_copy_to_stream_ex() drops progress - notifications when using the copy fast path). (David Carlier) - . Fixed bug GH-15836 (Use-after-free when a user stream filter accesses - $this->stream during the close flush). (iliaal) - -16 Jul 2026, PHP 8.6.0alpha2 - -- Core: - . Sync Boost.Context assembly with 1.91.0. (kn1g78) - . Fixed bug GH-22387 (AST pretty-printing drops meaningful parentheses around - RHS of instanceof). (timwolla) - . Fixed bug GH-15672 and GH-15911 (Stack overflow when an internal function - recurses through zend_call_function, such as a self-attached SPL - iterator). (iliaal) - . Lock unmodified readonly properties for modification after clone-with. - (NickSdot) - . abort() instead of exit() on hard OOM. (realFlowControl) - . perf: ZTS: move AG and SCNG into native __thread storage. (henderkes) - -- Calendar: - . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with - INT_MAX year). (arshidkv12) - -- Curl: - . Added CURLOPT_SEEKFUNCTION and the CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL - and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed - request body to resend it on a redirect, multi-pass authentication or a - retried reused connection. (GrahamCampbell) - -- Date: - . Update timelib to 2022.17. (Derick) - . Fixed bug GH-19803 (Parsing a string with a single white space does create - an error). (Derick) - . Fixed Unix timestamps in February of the year 0 are misparsed with - @-notation. (LukasGelbmann) - . Fixed bug GH-11368 (idate() doesn't work for the year -1). (Derick) - . Fixed bug GH-11310 (__debugInfo does nothing on userland classes extending - Date classes). (Derick) - -- DBA: - . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz) - -- DOM: - . Fixed bug GH-22570 (Stack overflow when serializing a deeply nested - Dom\XMLDocument). (iliaal) - . Fixed Dom\DtdNamedNodeMap integer dimension access so negative indexes - return NULL and indexes outside the int range throw ValueError instead of - returning the first entity or notation. (Weilin Du) - . Fixed bug GH-22623 (use after free with namespace nodes from - XSLTProcessor::registerFunctions())/ (David Carlier) - . Fixed bug GH-22554 (use-after-free with XPath callback returning a node - from a foreign document). (David Carlier) - -- Exif: - . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size" - warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) - -- GMP: - . Fixed bug GH-22549 (Assertion failure / UB on a compound GMP power or shift - assignment with a negative exponent). (iliaal) - -- Intl: - . Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to - reject offset values outside the 32-bit range instead of silently - truncating them. (Weilin Du) - . IntlDateFormatter::parse()/datefmt_parse() and - IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError - when the offset argument is not of type int. (Weilin Du) - -- JSON: - . Report unterminated JSON strings as syntax errors. (timwolla) - . Improve performance error position tracking during JSON decoding. - (henderkes) - . Fixed bug GH-22514 (Incorrect error column in PHP 8.6 JSON parser). - (henderkes, timwolla) - -- Opcache: - . Fixed bug GH-21770 (Infinite recursion in property hook getter in opcache - preloaded trait). (iliaal) - -- OpenSSL: - . Added $salt_length parameter to openssl_sign() and openssl_verify() with - new OPENSSL_RSA_PSS_SALTLEN_* constants. (Jakub Zelenka) - . Fixed timeout for supplemental read at end of a blocking stream in SSL - stream wrapper. (ilutov) - . Fixed stream_socket_get_crypto_status() after supplemental read. (ilutov) - -- PDO_ODBC: - . Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN - carries no credentials). (iliaal) - -- PHPDBG: - . Fixed fleaked lowercased lookup keys in phpdbg_resolve_opline_break. - (jorgsowa) - . Fixed off-by-one in phpdbg_safe_class_lookup() causing class lookups to - always fail during phpdbg's signal-safe interruption path. (jorgsowa) - -- Reflection: - . Fixed bug GH-22683 (Reflection(Class)Constant::__toString() should not warn - on NAN conversions). (Khaled Alam) - . Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes). - (DanielEScherzer) - -- Session: - . Fixed bug GH-21314 (Different session garbage collector behavior between - PHP 8.3 and PHP 8.5). (jorgsowa) - -- SOAP: - . Fixed bug GH-22585 (OOM on bailout with uninitialized - do_request() parameters). (David Carlier) - . Fixed xsd:hexBinary decoding to reject odd-length values instead of - silently truncating the last nibble. (Weilin Du) - . Made SOAP encoding errors report the affected type or failing operation - instead of the generic "Violation of encoding rules" message. (Weilin Du) - -- Standard: - . Fixed sleep() and usleep() to reject values that overflow the underlying - unsigned int timeout. (Weilin Du) - . Fixed bug GH-22671 (assert.bail aborts the process when the assert callback - throws an exception whose reporting re-throws). (iliaal) - . Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator - mutates the array being sorted). (azchin, iliaal) - -- Streams: - . Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL - and a proxy set). (CVE-2026-12184) (ndossche) - -- URI: - . Fixed bug GH-22628 (Percent-encoding of caret in WHATWG URL paths is not - performed). (kocsismate) - . Fixed bug GH-22629 (WHATWG Validation error incorrect with empty host and - non-empty userinfo). (kocsismate) - -- Zip: - . Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex() - could crash after overwriting an entry and resetting its inherited - unchanged comment). (Weilin Du) - . Fixed bug GH-21705 (ZipArchive::getFromIndex() ignores - ZipArchive::FL_UNCHANGED for deleted entries). (Weilin Du) - . Fixed bug GH-22176 (memory leak with ZipArchive::registerCancelBack() - is used with reference returning function during shutdown). - (David Carlier) - . ZipArchive::addGlob() and ZipArchive::addPattern() now raise a TypeError - for invalid "remove_all_path", "comp_method", "comp_flags", and - "enc_method" options instead of emitting a warning. (David Carlier) - -02 Jul 2026, PHP 8.6.0alpha1 - -- Core: - . Added first-class callable cache to share instances for the duration of the - request. (ilutov) - . It is now possible to use reference assign on WeakMap without the key - needing to be present beforehand. (ndossche) - . Added `clamp()`. (kylekatarnls, thinkverse) - . Fix OSS-Fuzz #429429090 (Failed assertion on unset() with uninitialized - container). (ilutov) - . Fixed GH-20564 (Don't call autoloaders with pending exception). (ilutov) - . Fix deprecation not showing when accessing null key of an array with JIT. - (alexandre-daubois) - . Fixed bug GH-20174 (Assertion failure in - ReflectionProperty::skipLazyInitialization after failed LazyProxy - initialization). (Arnaud) - . Enabled the TAILCALL VM on Windows when compiling with Clang >= 19 x86_64. - (henderkes) - . Deprecate specifying a nullable return type for __debugInfo(). (timwolla) - . Fixed bug GH-22142 (Assertion failure in zendi_try_get_long() on IS_UNDEF). - (David Carlier) - . Fixed bug GH-22046 (The unserialize function can lead to segfault when - non-Serializable internal classes are serialized back with the C format). - (kocsismate) - . Fixed bug GH-22292 (AST pretty printing does not correctly handle invalid - variable names). (timwolla) - . Fixed bug GH-22291 (AST pretty printing does not correctly handle braces in - string interpolation). (timwolla) - . Fixed bug GH-22373 (AST pretty-printing drops meaningful parentheses - surrounding property access). (timwolla) - . Fixed GH-22422 (zend_arena layout mismatch leaked memory in separately - built extensions under AddressSanitizer). (iliaal) - . TSRM: use local-exec TLS in PIE executables. (henderkes) - . perf: make all static extensions use TSRMG_STATIC. (henderkes) - . Fixed bug GH-22257 (type confusion in Exception::getTraceAsString()). - (David Carlier) - . TSRM: make CG, EG, SCNG and AG compile-time offsets. (henderkes) - . Deprecate returning values from __construct() and __destruct(). (timwolla) - . base_convert, bindex, hexdec and octdec now raise a notice when they cannot - precisely convert the given number. (Sjoerd Langkemper) - . Added error_include_args INI option to make the display of function - arguments consistent in error output. (Calvin Buckley) - -- BCMath: - . Added NUL-byte validation to BCMath functions. (jorgsowa) - -- BZ2: - . Reject oversized input in bzdecompress(). (arshidkv12) - -- Curl: - . Add support for CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). (Ayesh) - -- Date: - . Update timelib to 2022.16. (Derick) - -- DOM: - . Removed LIBXML_XINCLUDE from valid options for XMLDocument, as it was a - no-op. (ndossche) - . Readonly DOM properties are now declared with asymmetric visibility - (public private(set)). ReflectionProperty::isWritable() reports them - correctly, and external writes raise "Cannot modify private(set) - property" instead of the previous readonly modification error. - (David Carlier) - . Fixed Dom\Notation nodes missing tree connection, so that ownerDocument, - parentNode, isConnected and baseURI now return correct values, and - textContent returns NULL per the DOM specification. (jordikroon) - -- EXIF: - . Added support for reading EXIF metadata from WebP images (GH-19904). - (iliaal) - -- Fileinfo: - . Fixed bug GH-20679 (finfo_file() doesn't work on remote resources). - (ndossche) - . Fixed bug #66095 (Hide libmagic dynamic symbols). (orlitzky) - -- GD: - . imagesetstyle()/imagefilter()/imagecrop() check array argument entries - types. (David Carlier) - -- GMP: - . gmp_fact() reject values larger than unsigned long. (David Carlier) - . gmp_pow/binomial/root/rootrem and shift/pow operators reject values larger - than unsigned long. (David Carlier) - . GMP exponentiation and shift operators now emit a deprecation warning - when converting a float right operand to int loses precision. (Weilin Du) - -- Hash: - . Upgrade xxHash to 0.8.2. (timwolla) +?? ??? ????, PHP 8.7.0alpha1 - Intl: - . Fixed malformed ResourceBundle::get() error message when fallback is - disabled. (Weilin Du) - . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(), - with the alias of locale_get_display_keyword() and - locale_get_display_keyword_value() respectively. (Weilin Du) - . Fix incorrect argument positions for invalid start/end arguments in - transliterator_transliterate(). (Weilin Du) - . Fixed IntlTimeZone::getDisplayName() to synchronize object error state - for invalid display types. (Weilin Du) - . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the - fallback locale when a language tag cannot be canonicalized. (Weilin Du) - . Added IntlNumberRangeFormatter class to format an interval of two numbers - with a given skeleton, locale, collapse type and identity fallback. - (BogdanUngureanu) - . Fixed bug GH-20426 (Spoofchecker::setRestrictionLevel() error message - suggests missing constants). (DanielEScherzer) - . Added grapheme_strrev (Yuya Hamada) - . Passing a non-stringable object as a time zone to Intl time zone - argument handling now raises TypeError instead of Error. (Weilin Du) - . IntlBreakIterator::getLocale() now raises ValueError for invalid locale - types. (Weilin Du) - . Fixed MessageFormatter::parse() and parseMessage() returning PHP_INT_MIN - as float rather than int on 64-bit platforms. (Weilin Du) - . Fixed UConverter::transcode() silently truncating from_subst and to_subst - option lengths greater than 127 bytes. (Weilin Du) - . Fixed IntlIterator::current() to return NULL instead of an undefined value - when the iterator is not positioned on a valid element. (Weilin Du) - -- IO: - . Added new polling API. (Jakub Zelenka) - -- JSON: - . Enriched JSON last error / exception message with error location. - (Juan Morales) - -- Fibers: - . Fixed bug GH-20483 (ASAN stack overflow with fiber.stack_size INI small - value). (David Carlier) - -- Mail: - . Fixed bug GH-20862 (null pointer dereference in - php_mail_detect_multiple_crlf via error_log (jordikroon) - -- Mbstring: - . ini_set() with mbstring.detect_order changes the order of mb_detect_order - as intended, since mbstring.detect_order is an INI_ALL setting. (tobee94) - . Added GB18030-2022 to default encoding list for zh-CN. (HeRaNO) - . Fixed bug GH-20836 (Stack overflow in mb_convert_variables with - recursive array references). (alexandre-daubois) - . Fixed bug GH-21223; mb_guess_encoding no longer crashes when passed huge - list of candidate encodings (with 200,000+ entries). (Jordi Kroon) - . mbregex has been deprecated. (youkidearitai) - -- Mysqli: - . Added mysqli_quote_string() and mysqli::quote_string(). (Kamil Tekiela) - -- Opcache: - . Fixed bug GH-20051 (apache2 shutdowns when restart is requested during - preloading). (Arnaud, welcomycozyhom) - -- OpenSSL: - . Added AES-SIV support. (jordikroon) - . Implemented GH-20310 (No critical extension indication in - openssl_x509_parse() output). (StephenWall) - . Added TLS session resumption support for streams with new context options - and Openssl\Session class. (Jakub Zelenka) - . Added TLS external PSK support for streams with new context options and - Openssl\Psk class. (Jakub Zelenka) - . Added TLS 1.3 early data (0-RTT) support for streams with new context - options early_data, max_early_data and early_data_cb. (Jakub Zelenka) - . Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE. - (Jakub Zelenka) - -- PCNTL: - . pcntl_exec() now throws a ValueError if the $args array is not a list - array. (Weilin Du) - -- PDO_DBLIB: - . Added dblib_handle_check_liveness handler. (freddy77) - -- PDO_PGSQL: - . Clear session-local state disconnect-equivalent processing. - (KentarouTakeda) - -- PGSQL: - . Enabled 64 bits support for pg_lo_truncate()/pg_lo_tell() if the server - supports it. (KentarouTakeda) - . pg_fetch_object() now surfaces non-instantiable class errors before - fetching, resolves the constructor via the get_constructor handler, and - reports the empty-constructor ValueError on the $constructor_args argument. - (David Carlier) - -- Phar: - . Support reference values in Phar::mungServer(). (ndossche) - . Invalid values now throw in Phar::mungServer() instead of being silently - ignored. (ndossche) - . Fixed a bypass of the magic ".phar" directory protection in - Phar::addEmptyDir() for paths starting with "/.phar". (Weilin Du) - . Fixed an integer underflow when parsing ZIP extra fields. (Weilin Du) - . Phar::addEmptyDir() now allows non-magic directory names that merely - share the ".phar" prefix. (Weilin Du) - . Support overridden methods in SplFileInfo for getMTime() and getPathname() - when building a phar. (ndossche) - . Mark Phar::buildFromIterator() base directory argument as a path. - (ndossche) - -- phpdbg: - . Fixed GH-22480 (Use-after-free when re-watching an already-watched - variable). (iliaal) - -- Posix: - . Added validity check to the flags argument for posix_access(). (arshidkv12) - -- Reflection: - . Fixed bug GH-20217 (ReflectionClass::isIterable() incorrectly returns true - for classes with property hooks). (alexandre-daubois) - . Added ReflectionConstant::inNamespace(). (Khaled Alam) - . Added ReflectionProperty::isReadable() and ::isWritable(). (ilutov) - . Fixed bug GH-21362 (ReflectionMethod::invoke/invokeArgs() did not verify - Closure instance identity for Closure::__invoke()). (Ilia Alshanetsky) - . Added ReflectionParameter::getDocComment(). (chschneider) - -- Session: - . Fixed bug 71162 (updateTimestamp never called when session data is empty). - (Girgias) - . Null bytes in session.cookie_path, session.cookie_domain, and - session.cache_limiter are now rejected with a warning. (jorgsowa) - . session.cookie_samesite now rejects invalid values with a warning; only - "Strict", "Lax", "None", or "" are accepted. (jorgsowa) - . session.cookie_lifetime now rejects non-integer and out-of-range values - with a warning. (jorgsowa) - . Session file GC now recursively cleans nested subdirectories when - session.save_path uses the dirdepth prefix. (jorgsowa) - . Changed defaults of session.use_strict_mode (now 1), session.cookie_httponly - (now 1) and session.cookie_samesite (now "Lax"). (jorgsowa) - -- Shmop: - . Fixed bug GH-9945 (shmop_open() silently truncates keys outside the key_t - range). (Weilin Du) - -- Soap: - . Soap::__setCookie() when cookie name is a digit is now not stored and - represented as a string anymore but a int. (David Carlier) - . Fixed bug GH-21421 (SoapClient typemap property breaks engine assumptions). - (ndossche) - . WSDL/XML Schema parsing now rejects out-of-range integer values for - occurrence constraints and integer restriction facets. Negative minOccurs - and maxOccurs values are rejected as well. (Weilin Du) - -- Sockets: - . Added the TCP_USER_TIMEOUT constant for Linux to set the maximum time in - milliseconds transmitted data can remain unacknowledged. (James Lucas) - . Added AF_UNSPEC support for sock_addrinfo_lookup() as a sole umbrella for - AF_INET* family only. (David Carlier) - . Fixed GH-20532 (socket_addrinfo_lookup gives the error code with a new - optional parameter). (David Carlier) - . Added AF_PACKET support completion for socket_sendto()/socket_recvfrom(). - (David Carlier) - -- Sodium: - . Added support for libsodium 1.0.21 IPcrypt and XOF APIs. (jedisct1) - . pwhash argument-validation errors now throw ValueError instead of - SodiumException. (iliaal) - -- SPL: - . DirectoryIterator key can now work better with filesystem supporting larger - directory indexing. (David Carlier) - . Fixed bug GH-21831 (SplObjectStorage::removeAllExcept() use-after-free with - re-entrant getHash()). (Pratik Bhujel) - . Fix bugs GH-8561, GH-8562, GH-8563, and GH-8564 (Fixing various - SplFileObject iterator desync bugs). (iliaal) - . Fix bug GH-22062 (SplDoublyLinkedList iterator UAF via destructor releasing - next node). (David Carlier) - -- Sysvshm: - . Fixed shm_attach() to throw ValueError for keys outside the key_t range. - (Weilin Du) - -- Sqlite3: - . Fix NUL byte truncation in sqlite3 TEXT column handling. (ndossche) - -- Standard: - . Fixed bug GH-19926 (reset internal pointer earlier while splicing array - while COW violation flag is still set). (alexandre-daubois) - . Added form feed (\f) in the default trimmed characters of trim(), rtrim() - and ltrim(). (Weilin Du) - . Invalid mode values now throw in array_filter() instead of being silently - defaulted to 0. (Jorg Sowa) - . Fixed bug GH-21058 (error_log() crashes with message_type 3 and - null destination). (David Carlier) - . Fixed bug GH-13204 (glob() fails if square bracket is in current directory). - (ndossche) - . Add array size maximum to array_diff(). (ndossche) - . Add enum SortDirection. (timwolla) - . pathinfo() raises a ValueError with an invalid $flags argument. - (David Carlier) - . Passing an invalid flag value to the second argument of scandir() will now - throw a ValueError. (alexandre-daubois) - . array_change_key_case() now raises a ValueError when an invalid $case - argument value is passed. (Girgias) - . linkinfo() now raises a ValueError when the argument is an empty string. - (Weilin Du) - . getenv() and putenv() now raises a ValueError when the first argument - contains NUL bytes. (Weilin Du) - . dl() now raises a ValueError when the $extension_filename argument contains - NUL bytes. (Weilin Du) - . openlog() now raises a ValueError when the $prefix argument contains NUL - bytes. (Weilin Du) - . parse_str() now raises a ValueError when the $string argument contains NUL - bytes. (Weilin Du) - . proc_open() now raises a ValueError when the $cwd argument contains NUL - bytes. (Weilin Du) - . ini_get_all() now includes the built-in default value in the details. - (sebastian) - . Fixed bug GH-22171 (Invalid auth header generation in http(s) stream - wrapper). (David Carlier) - . Fixed bug GH-17384 (number_format() may exhaust memory with decimals - outside the range from -2147483648 to 2147483647). (Weilin Du) - -- Streams: - . Added new stream errors API including new StreamException, StreamError - classes, StreamErrorStore, StreamErrorMode, StreamErrorCode enums, - stream_last_errors() and stream_clear_errors() functions, error_mode, - error_store and error_handler stream context options and extending some - stream functions with context param. (Jakub Zelenka) - . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream - socket context options. (Jakub Zelenka) - . Added so_reuseaddr streams context socket option that allows disabling - address resuse. (Jakub Zelenka) - . Added so_linger stream socket context option. (Jakub Zelenka) - . Fixed bug GH-20370 (User stream filters could violate typed property - constraints). (alexandre-daubois) - . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka) - . Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream). - (ilutov) - . Improved stream_socket_server() bind failure error reporting. (ilutov) - . Fixed bug #49874 (ftell() and fseek() inconsistency when using stream - filters). (Jakub Zelenka) - -- URI: - . Added Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme(). - (kocsismate) - . Added Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType(). - (kocsismate) - . Added Uri\Rfc3986\UriBuilder. (kocsismate) - -- Zip: - . Fixed bug GH-21682 (ZipArchive instances should not be serializable). - serialize()/unserialize() now throw unless a subclass overrides - __serialize()/__unserialize(). (iliaal) - . Fixed ZipArchive callback being called after executor has shut down. - (ilutov) - . Support minimum version for libzip dependency updated to 1.0.0. - (David Carlier) - . Added ZipArchive::openString() method. - (Tim Starling, Soner Sayakci, Ghaith Olabi) - -- Zlib: - . deflate_init() now raises a TypeError when the value for option - "level", "memory", "window", or "strategy" is not of type int. - (Weilin Du) - . inflate_init() now raises a TypeError when the value for option - "window" is not of type int. (Weilin Du) + . Fixed Collator attribute and strength methods not rejecting an + unconstructed Collator. (Ilia Alshanetsky) <<< NOTE: Insert NEWS from last stable release here prior to actual release! >>> diff --git a/UPGRADING b/UPGRADING index 7f1fc588bd03..f66d5b6987f4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1,4 +1,4 @@ -PHP 8.6 UPGRADE NOTES +PHP 8.7 UPGRADE NOTES 1. Backward Incompatible Changes 2. New Features @@ -19,907 +19,30 @@ PHP 8.6 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== -- Core: - . By-reference foreach loops may now visit previously skipped elements - after array compaction. Internal pointers on deleted elements now move - to the next surviving element during copy-on-write. - . ??/empty() on a magic property no longer call __get() when __isset() - has materialized the property by writing into the property table. - The freshly-written value is returned directly. isset() is unaffected. - -- COM: - . It is no longer possible to clone variant objects because the cloning - behavior was ill-defined. - -- Curl: - . The callback registered with CURLOPT_READFUNCTION now throws a ValueError - when returning an integer other than 0, CURL_READFUNC_ABORT or - CURL_READFUNC_PAUSE. - -- DOM: - . Properties previously documented as @readonly (e.g. DOMNode::$nodeType, - DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, - DOMEntity::$encoding, DOMEntity::$version) are now declared with asymmetric - visibility (public private(set)). Attempts to write to them from outside - the class now raise "Cannot modify private(set) property ::$ - from global scope" instead of the prior readonly modification error. - ReflectionProperty::isWritable() also reports these properties - accurately. - . Array access on Dom\DtdNamedNodeMap objects now returns null for negative - integer indexes instead of returning the first node. - . Array access on Dom\DtdNamedNodeMap objects now raises a ValueError when - the integer index is greater than INT_MAX instead of overflowing to a - smaller index. - -- FTP: - . ftp_nb_fget(), ftp_nb_fput(), ftp_nb_get() and ftp_nb_put() now throw an - Error when the connection is already transferring, instead of emitting a - warning and returning false. ftp_close() already throws on the same - condition. - -- GD: - . imagesetstyle(), imagefilter() and imagecrop() filter the types / values of - their array arguments and raise a TypeError / ValueError accordingly. - . imageaffinematrixget() now enforces the documented array|float type for the - $options parameter, including the corresponding weak and strict typing - behavior. - -- GMP: - . gmp_fact() now throws a ValueError if $num does not fit into an unsigned - long. - . gmp_pow(), gmp_binomial(), gmp_root() and gmp_rootrem() now throw a - ValueError if their second argument does not fit into an unsigned long. - . The shift (<<, >>) and exponentiation (**) operators on GMP objects now - throw a ValueError when GMP right operands are outside the unsigned long range, - instead of silently truncating them. - . GMP integer string parsing now throws a ValueError for strings containing NUL - bytes, instead of silently truncating them. - . gmp_powm() modulo-by-zero now raises a DivisionByZeroError whose message - includes the function name and argument index ($modulus). - -- Intl: - . Passing a non-stringable object as a time zone to Intl APIs that accept - time zone objects or strings now raises a TypeError instead of an Error. - . IntlIterator::current() now returns null when called before the iterator is - positioned, or after the iterator becomes invalid, instead of exposing an - undefined value. - . IntlBreakIterator::getLocale() now raises a ValueError when the type is - neither Locale::ACTUAL_LOCALE nor Locale::VALID_LOCALE instead of - returning false. - . MessageFormatter::parse() and parseMessage() now return PHP_INT_MIN as - int, rather than float, on 64-bit platforms when parsing integer values. - . The $type parameter of IntlBreakIterator::getPartsIterator() has been - changed from string to int to match the underlying implementation. - . UConverter::transcode() now rejects from_subst and to_subst option values - longer than 127 bytes instead of silently truncating the length before - passing it to ICU. - . ResourceBundle::get() and resourcebundle_get() now report fallback-disabled - resource lookups with "without fallback to " instead of the - malformed "without fallback from to ". - . IntlDateFormatter::parse()/datefmt_parse() and - IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError - when the offset argument is not of type int instead of silently converting - the value. - . Collator::sort(), collator_sort(), Collator::asort(), and - collator_asort() now report UTF-8/UTF-16 conversion failures during - comparison through the intl error mechanism and return false. With - intl.use_exceptions enabled, these failures throw IntlException. Previously, - these paths emitted a warning and compared the value as an empty string. - -- MBstring: - . Unicode data tables have been updated to Unicode 18.0 - -- PCNTL: - . pcntl_alarm() now raises a ValueError if the seconds argument is - lower than zero or greater than the platform's UINT_MAX. - . pcntl_exec() now raises a ValueError if the $args argument is not a list - array. - -- PCRE: - . preg_grep() now returns false instead of a partial array when a PCRE - execution error occurs (e.g. malformed UTF-8 input with the /u modifier). - This is consistent with other preg_* functions. - -- PGSQL: - . pg_fetch_object() now reports the ValueError for a non-empty - $constructor_args on a class without a constructor on the - $constructor_args argument instead of $class. Errors raised when - the requested class is not instantiable (abstract, interface, enum) - now surface before the row is fetched. - -- Phar: - . Phar::mungServer() now raises a ValueError when an invalid argument value - is passed instead of being silently ignored. - . Phar::addEmptyDir() now rejects "/.phar" paths in addition to ".phar" - paths, and raises the same BadMethodCallException for attempts to create - the reserved magic ".phar" directory through that form. - . Phar::addEmptyDir() now treats non-magic names that merely share the - ".phar" prefix as ordinary directories. - . Files are only automatically interpreted as Phar archives when included - if ".phar" occurs as an extension in the filename component of their - paths. Previously, it could occur in a directory name or as part of an - extension such as ".pharma". - -- Session: - . Setting session.cookie_path, session.cookie_domain, or session.cache_limiter - to a value containing NUL bytes now emits a warning and leaves the setting - unchanged. Previously, NUL bytes were silently accepted: for cookie_path - and cookie_domain this caused the SAPI to drop the Set-Cookie header; for - cache_limiter the value was silently truncated at the NUL byte. - . A ValueError is thrown if $name is a string containing NUL bytes in - session_module_name(). - . session_encode() now returns an empty string instead of false for empty - sessions. It only returns false now when the session data could not be - encoded. This mainly happens with the default serialization handler - if a key contains the pipe | character. - . When session.lazy_write is enabled and a session handler implements - SessionUpdateTimestampHandlerInterface, sessions that were read as empty - and remain empty at write time will now trigger updateTimestamp() instead - of write(). Previously, write() was always called for empty sessions - because session_encode() returned false, bypassing the lazy_write - comparison. Custom session handlers that rely on write() being called - with empty data (e.g. to destroy the session) should implement the same - logic in their updateTimestamp() method. - . The defaults of three session INI settings have changed to provide secure - behavior out of the box: - - session.use_strict_mode is now 1 (was 0). Strict mode rejects - uninitialized session IDs, mitigating session fixation. Custom session - handlers that previously relied on accepting externally supplied IDs - without a corresponding storage entry must either implement - validateId() / create_sid() or explicitly set this to 0. - - session.cookie_httponly is now 1 (was 0). Session cookies are no - longer accessible to JavaScript via document.cookie. Applications - that read the session cookie from JavaScript must explicitly set - this to 0. - - session.cookie_samesite is now "Lax" (was unset). Session cookies - are no longer sent on cross-site requests other than top-level - navigations using safe HTTP methods. Applications that depend on - session cookies being sent on cross-site POST submissions must - explicitly set this to "None" (and also set session.cookie_secure - to 1). - RFC: https://wiki.php.net/rfc/session_security_defaults - . SessionHandler::validateId() has been added and delegates to the - configured save handler. A subclass that declares validateId() without - a return type now emits a deprecation notice for the tentative bool - return type. A subclass that overrides open() without calling - parent::open() keeps its previous behavior and emits a warning when an - ID is validated. - -- Shmop: - . shmop_open() now raises a ValueError when the $key argument is outside the - platform's key_t range instead of passing a truncated key to the operating - system. - -- SimpleXML: - . SimpleXMLElement::__construct() now raises a ValueError when the $data - argument contains NUL bytes, matching simplexml_load_file(). With - $dataIsURL set, it previously truncated the path at the first NUL byte. - Without it, the string went to libxml, which with default options rejects a - NUL on current versions but accepts the truncated document on older ones - and under LIBXML_RECOVER. - -- SOAP: - . The "classmap" option of SoapClient and SoapServer now rejects arrays - containing integer keys. Previously, sparse integer-keyed and mixed-keyed - arrays could be accepted. SoapClient now throws TypeError for non-array - "classmap" options and ValueError for arrays containing integer keys, also - when the "exceptions" option is disabled. - . WSDL/XML Schema parsing now rejects out-of-range integer values for - occurrence constraints and integer restriction facets. Negative minOccurs - and maxOccurs values are rejected as well. - . SOAP encoding errors now report the affected type or failing operation in - the error message instead of the generic "Encoding: Violation of encoding - rules" message. Code that compares the exact message may need to be - updated. - -- Sockets: - . socket_set_option() with SO_ATTACH_REUSEPORT_CBPF now requires an int - $value and a $level of SOL_SOCKET. Any other value type throws a TypeError - instead of being coerced, and any other level raises a warning and returns - false. - . socket_set_option() with SO_ATTACH_REUSEPORT_CBPF and a $value of 0 now - detaches the reuseport filter through SO_DETACH_REUSEPORT_BPF. It - previously used SO_DETACH_BPF, an alias of SO_DETACH_FILTER, which left the - reuseport program attached. - -- Sodium: - . The password-hashing functions sodium_crypto_pwhash(), - sodium_crypto_pwhash_str(), - sodium_crypto_pwhash_scryptsalsa208sha256() and - sodium_crypto_pwhash_scryptsalsa208sha256_str() now throw ValueError - instead of SodiumException when an argument is out of range, such as an - opslimit or memlimit below the documented minimum. SodiumException is - still thrown for internal libsodium failures. - -- SPL: - . SplObjectStorage::getHash() implementations may no longer mutate any - SplObjectStorage instance. Attempting to do so now throws an Error. - . SplFileObject::next() now advances the stream when no prior current() - call has cached a line. A subsequent current() call returns the new line - rather than the previous one. - . SplFileObject::fgets() no longer caches the returned line for subsequent - current() calls. current() now re-reads from the current stream position - instead of returning the line fgets() just returned. - . SplFileObject::next() past EOF no longer increments key() without bound. - SplFileObject::seek() past EOF now produces the same key() value as - SplTempFileObject; the two previously returned different values. - . DirectoryIterator::key() now returns int|string, - and DirectoryIterator::current() returns string|SplFileInfo|static. - -- Standard: - . array_intersect() with at least two arrays now converts values to strings - while scanning its inputs instead of during sort comparisons. This can - change the number and order of conversion warnings and __toString() calls, - which conversion exception is reached, and the result for stateful - __toString() implementations. Argument types are validated before checking - for empty arrays or converting values, so an invalid later argument can - suppress conversion side effects from earlier arrays. Values are not - converted if any input array is empty. - . Form feed (\f) is now added to the default trimmed characters of trim(), - rtrim() and ltrim(). - RFC: https://wiki.php.net/rfc/trim_form_feed - . array_filter() now raises a ValueError when an invalid $mode argument value - is passed. - . array_change_key_case() now raises a ValueError when an invalid $case - argument value is passed. - . getenv() and putenv() now raise a ValueError when the first argument - contains NUL bytes. - . dl() now raises a ValueError when the $extension_filename argument contains - NUL bytes. - . openlog() now raises a ValueError when the $prefix argument contains NUL - bytes. - . parse_str() now raises a ValueError when the $string argument contains NUL - bytes. - . setlocale() now raises a ValueError when a locale name contains NUL bytes, - instead of silently truncating it. - Arrays are now accepted only for the $locales argument. Passing an array as - a later variadic locale argument now throws a TypeError. Passing any - additional locale arguments when $locales is an array now throws an - ArgumentCountError. - . linkinfo() now raises a ValueError when the $path argument is empty. - . pathinfo() now raises a ValueError when an invalid $flags argument value is - passed. - . scandir() now raises a ValueError when an invalid $sorting_order argument - value is passed. - . number_format() now raises a ValueError when $decimals is outside the - integer range instead of silently clamping very large positive values. - . sleep() now raises a ValueError when $seconds is greater than the platform - limit (UINT_MAX seconds, or UINT_MAX / 1000 seconds on Windows) instead of - allowing the value to overflow. - . usleep() now raises a ValueError when $microseconds is greater than - UINT_MAX instead of allowing the value to overflow. - . proc_open() now raises a ValueError when the $cwd argument contains NUL - bytes. - . base_convert(), bindec(), hexdec() and octdec() now raise a notice when - they cannot precisely convert the given number. - . The following functions now raise a ValueError when the $filename argument - contains NUL bytes: - - fileperms() - - fileinode() - - filesize() - - fileowner() - - filegroup() - - fileatime() - - filemtime() - - filectime() - - filetype() - - is_writable() - - is_readable() - - is_executable() - - is_file() - - is_dir() - - is_link() - - file_exists() - - lstat() - - stat() - . unpack() now reads a "<" or ">" immediately following a format code as an - endianness modifier rather than as the first character of the element name. - Formats such as "sname" raises a ValueError because the C format code accepts no - endianness modifier. A name starting with these characters is unaffected - when a repeater precedes it, as in "s1prop = $val. - RFC: https://wiki.php.net/rfc/const_object_property_write - -- Curl: - . curl_getinfo() return array now includes a new size_delivered key, which - indicates the total number of bytes passed to the download write callback. - This value can also be obtained by passing CURLINFO_SIZE_DELIVERED as the - $option parameter. - Requires libcurl 8.20.0 or later. - . Added CURLOPT_SEEKFUNCTION to register a callback that repositions a - streamed request body so libcurl can rewind and resend it on a redirect, - multi-pass authentication, or a retried reused connection instead of - failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle, - offset and origin, and must return one of CURL_SEEKFUNC_OK, - CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK. - -- Date: - . Added a new Time\Duration class. - RFC: https://wiki.php.net/rfc/duration_class - -- Fileinfo: - . finfo_file() now works with remote streams. - -- GMP: - . Added gmp_powm_sec() for side-channel quiet modular exponentiation. - Requires GNU MP 5.0.0 or later; it is not available on official Windows - builds using MPIR. - . Added gmp_prevprime() to get the largest prime smaller than the given - number. The optional $definitely_prime output parameter indicates whether - the returned number is definitely prime, as opposed to probably prime. - A ValueError is thrown if no such prime exists. This function is available - only when PHP is built against GNU MP 6.3.0 or later; it is not available - on official Windows builds using MPIR. - -- Intl: - . Added the static methods IntlDatePatternGenerator::getSkeleton() and - IntlDatePatternGenerator::getBaseSkeleton() to generate the unique skeleton - and base skeleton for a date/time pattern. - . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(), - with the aliases locale_get_display_keyword() and - locale_get_display_keyword_value(), respectively. - RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue - . Added IntlNumberRangeFormatter class to format an interval of two numbers - with a given skeleton, locale, IntlNumberRangeFormatter::COLLAPSE_AUTO, - IntlNumberRangeFormatter::COLLAPSE_NONE, - IntlNumberRangeFormatter::COLLAPSE_UNIT, - IntlNumberRangeFormatter::COLLAPSE_ALL collapse and - IntlNumberRangeFormatter::IDENTITY_FALLBACK_SINGLE_VALUE, - IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, - IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and - IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks. - It is supported as of ICU 63. - . Added SpoofChecker::areBidiConfusable() to check whether two strings are - confusable for a given text direction, along with the SpoofChecker::LTR - and SpoofChecker::RTL direction constants. - It is supported as of ICU 74. - . Added SpoofChecker::getBidiSkeleton() to generate a confusable skeleton for - a given text direction. It is supported as of ICU 74. - . Added SpoofChecker::getSkeleton() to generate a confusable skeleton for a - given string. - -- IO: - . Added new polling API. - RFC: https://wiki.php.net/rfc/poll_api - -- JSON: - . Added extra info about error location to the JSON error messages returned - from json_last_error_msg() and JsonException message. - -- OpenSSL: - . Added TLS session resumption support for streams with new stream context - options: session_data, session_new_cb, session_cache, session_cache_size, - session_timeout, session_id_context, session_get_cb, session_remove_cb, - and num_tickets. This allows saving and restoring client sessions across - requests, implementing custom server-side session storage, and controlling - session cache behavior. - RFC: https://wiki.php.net/rfc/tls_session_resumption - . Added TLS external PSK support for streams with new stream context options: - psk_client_cb and psk_server_cb. This allows setting and receiving PSK. - . Added TLS 1.3 early data (0-RTT) support for streams. Clients send early - data with the early_data context option; servers accept it with - max_early_data and receive it through the early_data_cb callback. The - outcome is reported as 'accepted', 'rejected' or 'not_sent' in the - early_data key of the crypto stream_get_meta_data() array. - -- PDO_PGSQL: - . Added Pdo\Pgsql::ATTR_CHUNK_SIZE, the number of rows a statement fetches - per chunk. A value of 1 or more enters the lazy fetch mode of - PDO::ATTR_PREFETCH => 0. Setting PDO::ATTR_PREFETCH replaces the chunk - size. Statements that are prepared with neither it nor PDO::ATTR_PREFETCH - fall back to the value set on the connection. - Requires libpq 17 or later. - -- Phar: - . Overriding the getMTime() and getPathname() methods of SplFileInfo now - influences the result of the phar buildFrom family of functions. - This makes it possible to override the timestamp and names of files. - -- SNMP: - . It is now possible to use AES192, AES192C, AES256, and AES256C as - SNMPv3 security protocols if the underlying library supports them. - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#increase_the_number_of_snmpv3_security_protocols_supported - . It is now possible to reset the MIB tree using the new snmp_init_mib() - function. - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#allow_the_snmp_mib_to_be_reset - . Additional MIB parsing and output control functionality has been exposed - via the snmp_set_mib_option(), snmp_set_output_option(), - snmp_set_string_output_format() functions, the $numeric_index, - $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, - $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), - SNMP::setStringOutputFormat() methods. (eskyuu) - RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls - -- Standard: - . pack() and unpack() now accept the "<" and ">" endianness modifiers on - the signed and unsigned integer format codes. - RFC: https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support - . pack() and unpack() now accept the "<" and ">" endianness modifiers on - the float and double format codes. - RFC: https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier - -- Streams: - . Added new stream errors API including new classes, enums, functions and - internal API. It is controlled using error_mode, error_store and - error_handler stream context options. - RFC: https://wiki.php.net/rfc/stream_errors - . Added the "filter.max_filter_count" stream context option for php://filter - URLs. When set, opening the stream fails with a warning if the URL would - add more filters than the configured value. Negative values disable the - check. - RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains - . Added stream socket context option so_reuseaddr that allows disabling - address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on - Windows. - . Added stream socket context options so_keepalive, tcp_keepidle, - tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive - options. - . Added stream socket context option so_linger that sets SO_LINGER on TCP - sockets. A positive value enables lingering for that many seconds, zero - or a negative value disables it. Values above 65535 are clamped as the - linger time is limited to an unsigned short on some platforms. - . Added stream socket context options so_rcvbuf and so_sndbuf that set the - socket receive and send buffer sizes in bytes (SO_RCVBUF and SO_SNDBUF) on - TCP and UDP sockets. The value must be an integer between 1 and 2147483647, - any other value makes the stream creation fail. The operating system may - round, cap or otherwise adjust the requested size, and may stop sizing that - buffer automatically, so the size read back can differ from the one - requested. - . Allowed casting filtered streams as file descriptors for select. - . Added the "write_seek_mode" filter parameter for the bz2, iconv, - zlib, and string stream filters. This parameter must be set via an - associative array where the key is "write_seek_mode" and the - value is one of the following strings: "preserve", "reset", or "strict". - -- URI: - . Added Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme(). - RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection - . Added Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType(). - RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection - . Added Uri\Rfc3986\UriBuilder and Uri\WhatWg\UrlBuilder. - RFC: https://wiki.php.net/rfc/uri_followup#uri_building - . Added Uri\url_percent_encode(). - RFC: https://wiki.php.net/rfc/uri_followup#percent-encoding_support - ======================================== 3. Changes in SAPI modules ======================================== -- CLI: - . The built-in development server now accepts requests using the HTTP QUERY - method instead of returning 501 Not Implemented. - . The built-in development server no longer reflects the "Host" header from - requests. - ======================================== 4. Deprecated Functionality ======================================== -- Core: - . Using "namespace" as a class constant name is deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_using_namespace_as_a_class_constant_name - . Using the return statement in a finally block is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_returning_from_a_finally_block - . Specifying a return type of array|null / ?array for __debugInfo() is now - deprecated. Specify array instead. - . Returning values from __construct() and __destruct() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecate-return-value-from-construct - . Making __construct() and __destruct() a Generator is now deprecated. - RFC: https://wiki.php.net/rfc/deprecate-return-value-from-construct - . Naming a function readonly is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_the_possibility_to_name_a_function_readonly - . Passing a 3rd argument to define() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_define_with_case_insensitive_being_specified - . Calling is_a() or is_subclass_of() with a string as the first argument - when $allow_string is false is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_a_with_string_when_allow_string_is_false - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_subclass_of_with_string_when_allow_string_is_false - -- BZ2: - . Passing an object for the Bzip2 {de}compression stream filter is now - deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_bzip2decompress_and_bzip2compress_stream_filters - -- GMP: - . The shift (<<, >>) and exponentiation (**) operators on GMP objects now - emit a deprecation warning when converting a float right operand to int - loses precision. - -- Mbstring: - . Mbregex has been deprecated, because the underlying Oniguruma library - is no longer maintained. - RFC: https://wiki.php.net/rfc/eol-oniguruma - . Passing objects to mb_convert_variables() is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_vars_parameter_of_mb_convert_variables - -- MySQLi: - . The mysqli_get_charset() function and mysqli::get_charset() method are now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqli_get_charset - . The mysqli_stmt_init() function, mysqli::stmt_init() method, and calling the - mysqli_stmt constructor without providing the $query parameter are now - deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_mysqlistmt_init - -- Reflection: - . Calling ReflectionProperty::setValue() with an object that is not an - instance of the class on which the property was declared is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionpropertysetvalue_and_reflectionpropertysetrawvalue_with_wrong_types - . Calling ReflectionProperty::setRawValue() with an object that is not an - instance of the class on which the property was declared is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionpropertysetvalue_and_reflectionpropertysetrawvalue_with_wrong_types - . Calling ReflectionMethod::invoke() or ReflectionMethod::invokeArgs() with - an object and a static method is now deprecated. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionmethodinvoke_and_reflectionmethodinvokeargs_with_objects_for_static_methods - -- Session: - . It is now deprecated to pass an object that does not implement the - create_sid() and validateId() methods. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid - . A deprecation is now emitted when implementing SessionHandlerInterface on a - class which doesn't define the create_sid() or validateId() methods, as - those will be moved from SessionUpdateTimestampHandlerInterface and - SessionIdInterface to SessionHandlerInterface. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid - -- SPL: - . The spl_classes() function is now deprecated. Use - ReflectionExtension::getClassNames() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_classes - . The spl_object_hash() function is now deprecated. Use spl_object_id() - instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_spl_object_hash - . The following ArrayIterator methods are now deprecated: - * ArrayIterator::getFlags() - * ArrayIterator::setFlags() - * ArrayIterator::asort() - * ArrayIterator::ksort() - * ArrayIterator::uasort() - * ArrayIterator::uksort() - * ArrayIterator::natsort() - * ArrayIterator::natcasesort() - * ArrayIterator::unserialize() - * ArrayIterator::serialize() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_arrayiterator_methods_that_inherit_arrayobject_implementation - . The following SplFileObject methods are now deprecated: - * SplFileObject::fgetcsv() - * SplFileObject::fputcsv() - * SplFileObject::setCsvControl() - * SplFileObject::getCsvControl() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_splfileobject_csv_methods - -- Standard: - . metaphone() is deprecated. - Please use a userland phonetic matching library instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_metaphone_function - . Using more than 16 filters in a php://filter URL without configuring the - "filter.max_filter_count" stream context option now emits an E_DEPRECATED - warning. Use stream_filter_append() or configure this option explicitly. - RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains - . Passing an object to array_walk{_recursive} is now deprecated. Use - get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_array_parameter_of_array_walk_and_array_walk_recursive - . Passing an object as the $data argument to http_build_query() is now - deprecated. The interpretation of object values within $data as arrays - is also deprecated. Convert objects to arrays with get_object_vars() - before calling the function. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_data_parameter_of_http_build_query - . The is_double() function is now deprecated. Use is_float() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_double - . The is_long() and is_integer() functions are now deprecated. Use is_int() - instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_integer - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_long - . The doubleval() function is now deprecated. Use floatval() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_doubleval - . The strcoll() function is now deprecated. Use Collator::compare() instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_strcoll - . The SORT_LOCALE_STRING constant for the family of sort functions is now - deprecated. Use one of the following functions instead: - * Collator::sort() - * Collator::asort() - * Collator::sortWithSortKeys() - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_sort_locale_string_flag_for_sort_functions - -- Zlib: - . Passing an object for the zlib deflate and inflate stream filter is now - deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_as_parameters_to_the_zlibinflate_and_zlibdeflate_stream_filters - . Passing an object as the $option argument to deflate_init and inflate_init - is now deprecated. Use get_object_vars() on the object instead. - RFC: https://wiki.php.net/rfc/deprecations_php_8_6#passing_objects_for_options_parameter_of_deflate_init_and_inflate_init - ======================================== 5. Changed Functions ======================================== -- Date: - . The following DateTime, DateTimeImmutable, DateTimeZone, DateInterval, and DatePeriod methods now have tentative static return types: - DateTime::createFromInterface() - DateTimeImmutable::__set_state() - DateTimeImmutable::modify() - DateTimeImmutable::add() - DateTimeImmutable::sub() - DateTimeImmutable::setTimezone() - DateTimeImmutable::setTime() - DateTimeImmutable::setDate() - DateTimeImmutable::setISODate() - DateTimeImmutable::setTimestamp() - DateTimeImmutable::createFromInterface() - DateTimeZone::__set_state() - DateInterval::__set_state() - DatePeriod::__set_state() - -- Filter: - . filter_var_array() return type has been narrowed from array|false|null to - array|false. The function always establishes an array before filtering, so - null was never returned. filter_input_array() is unaffected: it still - returns null when the requested superglobal does not exist. - -- LDAP: - . ldap_free_result() return type has been narrowed from bool to true. The - function already always returned true. - -- MySQLi: - . The return structure of mysqli_get_charset() no longer contains the - undocumented "comment" element. The value of "charsetnr" is now set to a - constant 0 as this number was an implementation detail that should not have - been exposed to the public. - -- OpenSSL: - . Output of openssl_x509_parse() contains criticalExtensions listing all - critical certificate extensions. - . openssl_sign() and openssl_verify() now have an additional optional - argument $salt_length that allows controlling the RSA-PSS salt length - when OPENSSL_PKCS1_PSS_PADDING is used. It accepts an explicit length or - one of the new OPENSSL_RSA_PSS_SALTLEN_* constants. - -- PDO_DBLIB: - . When using persistent connections, there is now a liveness check in the - constructor. - -- Phar: - . Phar::mungServer() now supports reference values. - -- Readline: - . readline_completion_function() now declares true as its return type. The - function assigns a static callback and then tests whether the assignment - landed, which is a tautology; an invalid callback throws a TypeError via - ZPP before the function body is reached. - -- Sockets: - . socket_addrinfo_lookup() now has an additional optional argument $error_code - that, when not null, receives the error code on failure (one of the EAI_* - constants). - . socket_cmsg_space() return type has been narrowed from ?int to int. Every - failure path has thrown a ValueError since PHP 8.0, so null was never - returned. - -- Standard: - . header_register_callback() now declares true as its return type. It has not - been able to return false since PHP 8.0.0, when passing an invalid - callback started throwing a TypeError instead. - . register_tick_function() now declares true as its return type. It has - always returned true on success; an invalid callback throws a TypeError - via ZPP before the function body is reached. - . ini_get_all() now includes a "builtin_default_value" element for each - directive when $details is true. It holds the built-in default value of the - directive (or null if it has none), independent of values set in php.ini, - on the command line, or at runtime. - . fclose(), file_put_contents() and copy() now return false when flushing - or closing the stream fails. Previously such failures were silently - ignored. - -- Zip: - . zip_entry_close() return type has been narrowed from bool to true. The - function already always returned true. - ======================================== 6. New Functions ======================================== -- GMP: - . gmp_powm_sec() - . gmp_prevprime() - -- Intl: - . grapheme_strrev() - RFC: https://wiki.php.net/rfc/grapheme_strrev - . IntlDatePatternGenerator::getSkeleton() - . IntlDatePatternGenerator::getBaseSkeleton() - . Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() - RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue - . SpoofChecker::areBidiConfusable() - . SpoofChecker::getBidiSkeleton() - . SpoofChecker::getSkeleton() - -- MySQLi: - . Added mysqli::quote_string() and mysqli_quote_string(). - RFC: https://wiki.php.net/rfc/mysqli_quote_string - -- Reflection: - . ReflectionConstant::inNamespace() - . ReflectionProperty::isReadable() and ReflectionProperty::isWritable() - RFC: https://wiki.php.net/rfc/isreadable-iswriteable - . ReflectionParameter::getDocComment() - RFC: https://wiki.php.net/rfc/parameter-doccomments - . ReflectionAttribute::inNamespace() - . ReflectionAttribute::getNamespaceName() - . ReflectionAttribute::getShortName() - -- SNMP: - . snmp_init_mib() - . snmp_set_mib_option() - . snmp_set_output_option() - . snmp_set_string_output_format() - -- Sodium: - . sodium_crypto_ipcrypt_keygen() - . sodium_crypto_ipcrypt_encrypt() - . sodium_crypto_ipcrypt_decrypt() - . sodium_crypto_ipcrypt_nd_keygen() - . sodium_crypto_ipcrypt_nd_encrypt() - . sodium_crypto_ipcrypt_nd_decrypt() - . sodium_crypto_ipcrypt_ndx_keygen() - . sodium_crypto_ipcrypt_ndx_encrypt() - . sodium_crypto_ipcrypt_ndx_decrypt() - . sodium_crypto_ipcrypt_pfx_keygen() - . sodium_crypto_ipcrypt_pfx_encrypt() - . sodium_crypto_ipcrypt_pfx_decrypt() - . sodium_bin2ip() - . sodium_ip2bin() - . sodium_crypto_xof_shake128() - . sodium_crypto_xof_shake128_init() - . sodium_crypto_xof_shake128_update() - . sodium_crypto_xof_shake128_squeeze() - . sodium_crypto_xof_shake256() - . sodium_crypto_xof_shake256_init() - . sodium_crypto_xof_shake256_update() - . sodium_crypto_xof_shake256_squeeze() - . sodium_crypto_xof_turboshake128() - . sodium_crypto_xof_turboshake128_init() - . sodium_crypto_xof_turboshake128_update() - . sodium_crypto_xof_turboshake128_squeeze() - . sodium_crypto_xof_turboshake256() - . sodium_crypto_xof_turboshake256_init() - . sodium_crypto_xof_turboshake256_update() - . sodium_crypto_xof_turboshake256_squeeze() - . sodium_crypto_kem_keypair(), sodium_crypto_kem_seed_keypair(), - sodium_crypto_kem_secretkey(), sodium_crypto_kem_publickey(), - sodium_crypto_kem_enc() and sodium_crypto_kem_dec() expose the X-Wing - KEM (hybrid ML-KEM768+X25519, libsodium's recommended KEM). - Available when PHP is built against libsodium >= 1.0.22. - . sodium_crypto_kem_mlkem768_keypair(), - sodium_crypto_kem_mlkem768_seed_keypair(), - sodium_crypto_kem_mlkem768_secretkey(), - sodium_crypto_kem_mlkem768_publickey(), - sodium_crypto_kem_mlkem768_enc() and sodium_crypto_kem_mlkem768_dec() - expose the ML-KEM768 (FIPS 203) KEM. - Available when PHP is built against libsodium >= 1.0.22. - -- Standard: - . clamp() returns the given value if in range, else returns the nearest - bound. - RFC: https://wiki.php.net/rfc/clamp_v2 - . stream_last_errors() and stream_clear_errors() - RFC: https://wiki.php.net/rfc/stream_errors - . stream_socket_get_crypto_status() - -- URI: - . Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme() - RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection - . Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType() - RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection - -- Zip: - . ZipArchive::openString() - . ZipArchive::closeString() - ======================================== 7. New Classes and Interfaces ======================================== -- Date: - . Time\Duration - RFC: https://wiki.php.net/rfc/duration_class - . Time\TimeException - RFC: https://wiki.php.net/rfc/duration_class - -- Intl: - . IntlNumberRangeFormatter - -- OpenSSL: - . Openssl\OpensslException - . Openssl\Session - RFC: https://wiki.php.net/rfc/tls_session_resumption - . Openssl\Psk - -- SNMP: - . enum: Snmp\Mib - . enum: Snmp\OidOutput - . enum: Snmp\Output - . enum: Snmp\StringOutput - -- Standard: - . enum SortDirection - RFC: https://wiki.php.net/rfc/sort_direction_enum - . StreamError - . StreamException - . enum StreamErrorStore - . enum StreamErrorMode - . enum StreamErrorCode - RFC: https://wiki.php.net/rfc/stream_errors - . Io\Poll\Context - . Io\Poll\Watcher - . enum Io\Poll\Backend - . enum Io\Poll\Event - . interface Io\Poll\Handle - . Io\IoException - . Io\Poll\PollException - . Io\Poll\FailedPollOperationException - . Io\Poll\FailedContextInitializationException - . Io\Poll\FailedHandleAddException - . Io\Poll\FailedWatcherModificationException - . Io\Poll\FailedPollWaitException - . Io\Poll\BackendUnavailableException - . Io\Poll\InactiveWatcherException - . Io\Poll\HandleAlreadyWatchedException - . Io\Poll\InvalidHandleException - . StreamPollHandle - RFC: https://wiki.php.net/rfc/poll_api - -- URI: - . Uri\Rfc3986\UriBuilder and Uri\WhatWg\UrlBuilder - RFC: https://wiki.php.net/rfc/uri_followup#uri_building - . enum Uri\WhatWg\UrlPercentEncodingMode - RFC: https://wiki.php.net/rfc/uri_followup#percent-encoding_support - ======================================== 8. Removed Extensions and SAPIs ======================================== @@ -928,236 +51,22 @@ PHP 8.6 UPGRADE NOTES 9. Other Changes to Extensions ======================================== -- Fileinfo: - . Upgraded to file 5.48. - Custom compiled magic databases from older file versions must be regenerated. - -- Hash: - . The bundled version of xxHash was upgraded to 0.8.2. - -- MySQLi: - . Added new constant MYSQLI_OPT_COMPRESS. - -- Opcache: - . JIT is now supported for ZTS builds on Apple Silicon. - ======================================== 10. New Global Constants ======================================== -- Curl: - . CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). - . CURLOPT_SEEKFUNCTION. - . CURL_SEEKFUNC_OK. - . CURL_SEEKFUNC_FAIL. - . CURL_SEEKFUNC_CANTSEEK. - . CURL_READFUNC_ABORT. - -- MySQLi: - . MYSQLI_OPT_COMPRESS. - -- OpenSSL: - . OPENSSL_RSA_PSS_SALTLEN_DIGEST. - . OPENSSL_RSA_PSS_SALTLEN_AUTO. - . OPENSSL_RSA_PSS_SALTLEN_MAX. - -- Sockets: - . TCP_USER_TIMEOUT (Linux only). - . AF_UNSPEC. - . EAI_BADFLAGS. - . EAI_NONAME. - . EAI_AGAIN. - . EAI_FAIL. - . EAI_NODATA. - . EAI_FAMILY. - . EAI_SOCKTYPE. - . EAI_SERVICE. - . EAI_ADDRFAMILY. - . EAI_SYSTEM. - . EAI_OVERFLOW. - . EAI_INPROGRESS. - . EAI_CANCELED. - . EAI_NOTCANCELED. - . EAI_ALLDONE. - . EAI_INTR. - . EAI_IDN_ENCODE. - . SO_DETACH_REUSEPORT_BPF (Linux only). - -- Sodium: - . SODIUM_CRYPTO_IPCRYPT_BYTES. - . SODIUM_CRYPTO_IPCRYPT_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_TWEAKBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_INPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_ND_OUTPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_TWEAKBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_INPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_NDX_OUTPUTBYTES. - . SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES. - . SODIUM_CRYPTO_IPCRYPT_PFX_BYTES. - . SODIUM_CRYPTO_XOF_SHAKE128_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_SHAKE128_STATEBYTES. - . SODIUM_CRYPTO_XOF_SHAKE256_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_SHAKE256_STATEBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE128_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE128_STATEBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE256_BLOCKBYTES. - . SODIUM_CRYPTO_XOF_TURBOSHAKE256_STATEBYTES. - . SODIUM_CRYPTO_KEM_PUBLICKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SECRETKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_CIPHERTEXTBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SHAREDSECRETBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_SEEDBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_KEYPAIRBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SECRETKEYBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SHAREDSECRETBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_SEEDBYTES (libsodium >= 1.0.22). - . SODIUM_CRYPTO_KEM_MLKEM768_KEYPAIRBYTES (libsodium >= 1.0.22). - -- Standard: - . ARRAY_FILTER_USE_VALUE. - . STREAM_CRYPTO_STATUS_NONE. - . STREAM_CRYPTO_STATUS_WANT_READ. - . STREAM_CRYPTO_STATUS_WANT_WRITE. - ======================================== 11. Changes to INI File Handling ======================================== -- Core: - . The error_include_args INI option has been added to make the display of - function arguments consistent in errors; it is off by default. Previously, - some functions shown a parameter in the error that was up to each call to - the internal error function. Now, all parameters as were actually passed to - the function will be displayed. This uses the same infrastructure as stack - traces, so i.e. sensitive parameters will not be displayed, and strings - will be truncated according to zend.exception_string_param_max_len. - RFC: https://wiki.php.net/rfc/display_error_function_args - -- Mbstring: - . The mbstring.detect_order INI directive now updates the internal detection - order when changed at runtime via ini_set(). Previously, runtime changes - using ini_set() did not take effect for mb_detect_order(). Setting the - directive to NULL or an empty string at runtime now leaves the previously - configured detection order unchanged. - -- MySQLi: - . mysqli.default_port now checks the validity of the value which should be - between 0 and 65535 inclusive. - -- Opcache: - . opcache.jit_debug accepts a new flag: ZEND_JIT_DEBUG_TRACE_EXIT_INFO_SRC. - When used along with ZEND_JIT_DEBUG_TRACE_EXIT_INFO, the source of exit - points is printed in exit info output, in debug builds. - ======================================== 12. Windows Support ======================================== -- Core: - . The official Windows builds now use Visual Studio 2026 (VS18). - -- LibXML: - . The libxml2 library used by the official Windows builds has been upgraded - to version 2.15.3. As a result, DOMDocument::$documentURI for documents - loaded from a local file now contains a native filesystem path instead of - a file URI. - -- OpenSSL: - . The OpenSSL library used by the official Windows builds has been upgraded - to OpenSSL 4. - ======================================== 13. Other Changes ======================================== -- Core: - . In case of a hard OOM PHP now calls abort() instead of exit(1), changing - the exit code to 134 and possibly creating a core dump. - . The PHP_OS_FAMILY constant has an AIX value for when running on AIX or - IBM i via PASE. - ======================================== 14. Performance Improvements ======================================== - -- Core: - . printf() using only "%s" and "%d" will be compiled into the equivalent - string interpolation, avoiding the overhead of a function call and - repeatedly parsing the format string. - . Arguments are now passed more efficiently to known constructors (e.g. when - using new self()). - . array_map() using a first-class callable or partial function application - callback will be compiled into the equivalent foreach-loop, avoiding the - creation of intermediate Closures, the overhead of calling userland - callbacks from internal functions and providing for better insight for the - JIT. - . The performance of the TAILCALL VM has been improved. - . The TAILCALL VM is now enabled on Windows when compiling with Clang >= 19 - on x86_64. - . The performance of ZTS builds has been improved. - . Added stateless closure cache. - RFC: https://wiki.php.net/rfc/closure-optimizations#stateless_closure_caching - . Deeply recursive code that causes the VM to allocate new stack pages should - now be faster. - -- DOM: - . Made splitText() faster and consume less memory. - -- GD: - . imagebmp(), imagewbmp(), imagegd(), and imagegd2() now buffer output when - writing to PHP streams, significantly improving performance when writing - images to files. - . Improved performance of imagegrabscreen() and imagegrabwindow() on - Windows. - -- Intl: - . Improved performance of IntlCalendar::getAvailableLocales() and - IntlDateFormatter::localtime() / datefmt_localtime() by pre-allocating - their returned arrays. - . Improved performance of transliterator_list_ids() and - resourcebundle_locales() by pre-allocating their returned arrays. - . Optimized callback invocation in IntlChar::enumCharTypes(). - -- JSON: - . Improve performance of encoding arrays and objects. - . Improved performance of indentation generation in json_encode() - when using PHP_JSON_PRETTY_PRINT. - -- Mbstring: - . Improved performance of mb_str_pad(). - -- Phar: - . 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(). - . Improved performance of array_map() with multiple arrays passed. - . Improved performance of array_sum() and array_product() for - integer-only arrays. - . Improved performance of array_unshift(). - . Improved performance of array_walk(). - . Improved performance of intval('+0b...', 2) and intval('0b...', 2). - . Improved performance of pathinfo() when requesting a single component. - . Improved performance of str_split(). - . Improved performance of str_pad(). - . Improved performance of str_repeat() when the multiplier is 1. - -- URI: - . Improved performance of Uri\WhatWg\Url::parse() when collecting - validation errors by pre-allocating the error array. - . Reduced allocations when reading IPv6/IPvFuture hosts and paths with - Uri\Rfc3986\Uri. - . Improved performance and memory consumption when using normalizing - (non-raw) getters on already-normalized URIs with Uri\Rfc3986\Uri. - -- Zip: - . Improved performance of ZipArchive::addGlob() and - ZipArchive::addPattern() by pre-allocating their returned arrays. - . Avoid string copies in ZipArchive::addFromString(). diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 8bbdc5caabe5..41432be1e429 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -1,4 +1,4 @@ -PHP 8.6 INTERNALS UPGRADE NOTES +PHP 8.7 INTERNALS UPGRADE NOTES 1. Internal API changes @@ -14,356 +14,18 @@ PHP 8.6 INTERNALS UPGRADE NOTES 1. Internal API changes ======================== -- Breaking changes: - . String formatting functions now support the custom conversion specifiers - 'pS' (zend_string*) and 'pp' (same as 'p'). Following the 'p' specifier with - an alpha-numeric character other than 'S' or 'p' is now an error. - - Examples: - - zend_string *str; - zend_spprintf("%pS", str); // valid, same as "%S" - zend_spprintf("%pp", str); // valid, same as "%p" - zend_spprintf("%pA", str); // invalid - zend_spprintf("%ppA", str); // valid, same as zend_spprintf("%p%c", str, 'A') - -- Removed: - . The misnamed ZVAL_IS_NULL() has been removed. Use Z_ISNULL() instead. - . The zval_is_true() alias of zend_is_true() has been removed. Call - zend_is_true() directly instead. - . The _zval_get_*() compatibility macros for PHP 7.2 have been removed - call the variant without the leading underscore instead. - Affected: _zval_get_long, _zval_get_double, _zval_get_string, - _zval_get_long_func, _zval_get_double_func, _zval_get_string_func - . CHECK_ZVAL_NULL_PATH() and CHECK_NULL_PATH() have been removed, use - zend_str_has_nul_byte(Z_STR_P(...)) and zend_char_has_nul_byte() - respectively. - . ZEND_LTOA() (and ZEND_LTOA_BUF_LEN) has been removed, as it was - unsafe. Directly use ZEND_LONG_FMT with a function from the - printf family. - . The zval_dtor() alias of zval_ptr_dtor_nogc() has been removed. - Call zval_ptr_dtor_nogc() directly instead. - . The internal zend_copy_parameters_array() function is no longer exposed. - . The internal zend_hash_minmax() function is no longer exposed. Scan the - HashTable directly and use zend_compare() for value comparisons instead. - . The zend_make_callable() function has been removed, if a callable zval - needs to be obtained use the zend_get_callable_zval_from_fcc() function - instead. If this was used to store a callable, then an FCC should be - stored instead. - . The zend_exception_save() and zend_exception_restore() functions were - removed. - . The zend_set_hash_symbol() API has been removed. - . The WRONG_PARAM_COUNT and ZEND_WRONG_PARAM_COUNT() macros have been - removed. Call zend_wrong_param_count(); followed by RETURN_THROWS(); - instead. - . PHP_HAVE_STREAMS macro removed from . - . The INI_STR(), INI_INT(), INI_FLT(), and INI_BOOL() macros have been - removed. Instead new zend_ini_{bool|long|double|str|string}_literal() - macros have been added. This fixes an internal naming inconsistency as - "str" usually means zend_string*, and "string" means char*. - However INI_STR() returned a char* - . The INI_ORIG_{INT|STR|FLT|BOOL}() macros have been removed as they are - unused. If this behaviour is required fall back to the zend_ini_* - functions. - . The unused ZEND_AST_PARENT_PROPERTY_HOOK_CALL has been removed. - . The EMPTY_SWITCH_DEFAULT_CASE() macro has been removed. Use - default: ZEND_UNREACHABLE(); instead. - . The ZEND_RESULT_CODE type has been removed. Use zend_result directly. - . The zend_parse_parameters_none_throw(), zend_parse_parameters_throw(), - and ZEND_PARSE_PARAMS_THROW have been removed due to being misleading, - since ZPP always throws, unless ZEND_PARSE_PARAMS_QUIET is given. Use - the non-throw versions. - . The XtOffsetOf() alias of C’s offsetof() macro has been removed. Use - offsetof() directly. - . The deprecated Z_COPYABLE(), Z_COPYABLE_P(), Z_OPT_COPYABLE(), and - Z_OPT_COPYABLE_P() macros have been removed. Check for IS_ARRAY directly. - . The deprecated Z_IMMUTABLE(), Z_IMMUTABLE_P(), Z_OPT_IMMUTABLE(), and - Z_OPT_IMMUTABLE_P() macros have been removed. Check for - IS_ARRAY && !REFCOUNTED directly. - . The unused Z_GC_*() macros have been removed. Use the corresponding - GC_*() macro on the result of Z_COUNTED(). - . The zend_binary_zval_strcmp() and zend_binary_zval_strncmp() functions - have been removed, because they are unsafe by relying on the zvals - having a specific type. Use zend_binary_strcmp() / zend_binary_strncmp(), - string_compare_function() or similar instead. - . The OPENBASEDIR_CHECKPATH() compatibility macro has been removed, instead - use php_check_open_basedir() directly. - . The Z_CONSTANT(), Z_CONSTANT_P(), Z_OPT_CONSTANT(), and - Z_OPT_CONSTANT_P() macros have been removed. Check for IS_CONSTANT_AST - directly. - . The {_}php_stream_fopen_with_path() functions have been removed as they are - unused. - . The php_error_docref1() and php_error_docref2() functions have been - removed, instead rely on the error_include_args INI option to show the - arguments to functions in a consistent manner. - . The following PHP stream functions prefixed with _ have been removed, - and the macro without it has become the canonical function name: - * _php_stream_cast() - * _php_stream_free_enclosed() - * _php_stream_free() - * _php_stream_seek() - * _php_stream_tell() - * _php_stream_read() - * _php_stream_write() - * _php_stream_fill_read_buffer() - * _php_stream_printf() - * _php_stream_eof() - * _php_stream_getc() - * _php_stream_putc() - * _php_stream_flush() - * _php_stream_sync() - * _php_stream_get_line() - * _php_stream_puts() - * _php_stream_stat() - * _php_stream_mkdir() - * _php_stream_rmdir() - * _php_stream_readdir() - * _php_stream_set_option() - * _php_stream_get_url_stream_wrappers_hash() - * _php_get_stream_filters_hash() - * _php_stream_mmap_unmap() - * _php_stream_mmap_unmap_ex() - * _php_stream_filter_prepend() - * _php_stream_filter_append() - * _php_stream_filter_flush() - . The PHP stream function _php_stream_stat_path() has been renamed to - php_stream_stat_path_ex() - . The PHP stream function _php_stream_scandir() was removed, - insted the PHP macro php_stream_scandir() is now a function as the - flags parameter was never used. - . The PHP stream function _php_stream_flush() was removed, - instead the PHP macro php_stream_flush() is now a proper function. - . The zend_save_error_handling() function was removed. - . The zend_parse_parameter() function has been removed, use one fo the - zend_parse_arg_TYPE() APIs instead. - . The zend_is_countable() function was removed. - -- Changed: - . Internal functions that return by reference are now expected to - automatically unwrap references when the result of the call is stored in an - IS_TMP_VAR variable. This may be achieved by calling the - zend_return_unwrap_ref() function. - . ZEND_AST_METHOD_REFERENCE has been renamed to - ZEND_AST_TRAIT_METHOD_REFERENCE. - . Functions using zend_forbid_dynamic_call() *must* be flagged with - ZEND_ACC2_FORBID_DYN_CALLS (@forbid-dynamic-calls in stubs). In debug - builds, failing to include that flag will lead to assertion failures. - . The zend_get_call_trampoline_func() API now takes the __call or - __callStatic zend_function* instead of a CE and a boolean argument. - . ZSTR_INIT_LITERAL(), zend_string_starts_with_literal(), and - zend_string_starts_with_literal_ci() now support strings containing NUL - bytes. Passing non-literal char* is no longer supported. - . The zend_active_function{_ex}() functions now return a const zend_function - pointer. - . zend_function.arg_info is now always a zend_arg_info*. Before, it was a - zend_internal_arg_info on internal functions, unless the - ZEND_ACC_USER_ARG_INFO flag was set. - . ZEND_INI_GET_ADDR() is now a void* pointer instead of a char* pointer. This - more correctly represents the generic nature of the returned pointer and - allows to remove explicit casts, but possibly breaks pointer arithmetic - performed on the result. - . The zend_dval_to_lval_cap() function no longer takes a second - zend_string* parameter. - . EG(in_autoload) was renamed to EG(autoload_current_classnames) and no - longer is a pointer, but a directly embedded HashTable struct. - . Extended php_stream_filter_ops with seek method. - . php_print_info_htmlhead() now takes a title argument. - . zend_argument_error_variadic() now takes a new 'function' parameter. - . The param argument in the php_verror() function has been removed. - . The php_stream_wrapper_log_error() signature changed from - (wrapper, options, fmt, ...) to - (wrapper, context, options, severity, terminating, code, fmt, ...). - To keep the previous behaviour pass NULL for the context, or the context at - hand if there is one, E_WARNING for the severity, and - ZEND_ENUM_StreamErrorCode_Generic for the code. terminating should be true - only if the error aborts the operation. - . zend_create_closure(), zend_create_fake_closure() and - zend_create_partial_closure() now take the bound $this as a zend_object* - instead of a zval*. Accordingly, zend_get_closure_this_ptr() now returns - that zend_object*, or NULL when the closure is unbound, instead of a - zval* that is IS_UNDEF when the closure is unbound. - . object_properties_load() now verifies that the given value is assignable - to typed properties. The check is performed in strict mode. - -- Added: - . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were - added, given the primary flags were running out of bits. - . Added zend_hash_str_lookup(). - . Added zend_ast_call_get_args() to fetch the argument node from any call - node. - . Added Z_PARAM_ENUM(). - . Added PHP_GD_Z_PARAM_ARRAY_HT_OR_DOUBLE() in ext/gd to parse array|float - arguments into either a HashTable pointer or a double. - . Added zend_enum_fetch_case_id(). - . Added zend_enum_get_case_by_id(). - . Added zend_bin2hex() and zend_bin2hex_str() as helper functions to remove - dependencies on /ext/hash in various extensions. - . Added a C23_ENUM() helper macro to define forward-compatible fixed-size - enums. - . Added zend_fcall_info.consumed_args together with - zend_fci_consumed_arg(), which allows moving a selected callback argument - instead of copying it in zend_call_function(). Currently only a single - consumed argument is supported. - . Added ZEND_CONTAINER_OF(). - . Added zend_reflection_property_set_raw_value_without_lazy_initialization(), - zend_reflection_property_set_raw_value() to expose the functionality of - ReflectionProperty::setRawValueWithoutLazyInitialization() and - ReflectionProperty::setRawValue() to C extensions. - . Added zend_object_set_properties_reinitable() to centralise temporarily - allowing reinitialisation of initialised readonly properties during - controlled operations such as cloning and unserialisation. - . Added zend_argument_error_ex(), zend_argument_type_error_ex(), - zend_argument_value_error_ex(). - . Added zend_ast_dup(). - . Added zend_compile_ast(). - . Added zend_check_type_ex(). - . Added zend_create_partial_closure(). - . Added a new IO copy API in . php_io_copy() copies bytes between - file descriptors using the most efficient platform primitive available - (sendfile, splice, copy_file_range, TransmitFile), and is now used by - php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed. - . Added zend_string_equals_cstr_ci(). - . Added zend_cstr_append_char(), zend_cstr_concat(), and - zend_cstr_concat3() as helper functions to allocate NUL-terminated raw C - strings from one or more buffers. - . Added zend_string_ends_with() and related variants. - . Added trait support for internal classes. - . Added do_php_cli(). - . Added zval_try_get_double(), which converts a defined zval to a double and - reports conversion failures through a bool pointer. String conversion uses - the numeric-string semantics of zval_try_get_long(), rather than the - zend_strtod() semantics of zval_get_double(); non-numeric strings such as - "INF" and "NAN" fail, while leading-numeric strings emit E_WARNING. When - *failed is true, the returned value must not be used and an exception may - already be pending. Passing an IS_UNDEF zval is a caller error. - ======================== 2. Build system changes ======================== -- Abstract: - . run-tests.php now runs in parallel by default, using up to 10 automatically - detected workers. Pass -j1 for sequential execution. --asan, --msan, and - Valgrind default to at most two workers. - . Minimum required PHP version found on the host system for running scripts - like build/gen_stub.php during development has been updated from 7.4 to 8.1. - . build/gen_stub.php may now generate a _decl.h file in addition to - the _arginfo.h file, if the stub declares enums and is annotated with - @generate-c-enums. For each enum the file will contain a C enum. Enum - values can be compared to the result of - zend_enum_fetch_case_id(zend_object*). - -- Unix build system changes: - . scripts/dev/update-autoconf.sh has been added to update config.*/libtool. - . libtool has been upgraded to 2.5.4 (serial 63), which fixes many bugs. - . As part of the upgrade to the new libtool: - . libtool is now spread across multiple files. phpize has been updated to - handle this. - . On macOS, libtool now uses -undefined dynamic_lookup for shared objects, - instead of -undefined suppress -flat_namespace. - . --with-pic is now --enable-pic. The old flag will result in an error. - . Symbol HAVE_ST_BLOCKS has been removed from php_config.h (use - HAVE_STRUCT_STAT_ST_BLOCKS). - . Added a new configure option --disable-apache2-conf to prevent apxs from - editing httpd.conf during installation. - -- Windows build system changes: - . Function SETUP_OPENSSL() doesn't accept 6th argument anymore and doesn't - define the HAVE_OPENSSL_SSL_H preprocessor macro anymore. - . Function SETUP_SQLITE3() doesn't define HAVE_SQLITE3_H and HAVE_SQLITE3EXT_H - preprocessor macros anymore. - . Added a new function CHECK_HEADER() which is intended to be used instead of - the CHECK_HEADER_ADD_INCLUDE(). - -- Embed: - . The CLI SAPI can not be disabled when building the embed SAPI - (--enable-embed is incompatible with --disable-cli). - ======================== 3. Module changes ======================== -- ext/date: - . php_idate() now returns the result state, and moves the return value into an - out parameter. - -- ext/intl: - . Added intl_icu_compat.h with helpers and feature macros for ICU - version-specific API differences. Code in ext/intl should use the - intl_icu_compat_* helpers and INTL_ICU_HAS_* macros instead of adding - direct U_ICU_VERSION_* guards for supported ICU API variants. - . The internal grapheme_get_break_iterator() helper no longer accepts a - stack buffer argument; pass only the UErrorCode* status argument. - . Added PHP_INTL_FUNCTION_WITH_ERROR_RESET() for procedural functions that - reset the global error. Use it instead of PHP_FUNCTION() followed by a - manual intl_error_reset(NULL) call. - . IC_METHOD() now resets the global error before entering the method - implementation. IntlChar methods should no longer reset it manually. - -- ext/mbstring: - . Added GB18030-2022 to default encoding list for zh-CN. - -- ext/mysqlnd: - . Dropped session_options parameter from all methods in mysqlnd_auth. - The same information is present in conn->options and should be used - instead. - . Removed charsets plugin. - -- ext/session: - . php_session_flush() now returns a bool rather than a zend_result. - . The mod_user_names global has been removed. - . The mod_user_uses_object_methods_as_handlers global has been added, - it indicates whether the session handlers are methods of an object or not. - . Removed session_adapt_url(). - . PS_OPEN_ARGS is now defined as - `void **mod_data, zend_string *save_path, zend_string *session_name` - rather than - `void **mod_data, const char *save_path, const char *session_name` - . PS_FUNCS() now includes the PS_VALIDATE_SID_FUNC() - . PS_MOD() now requires that the PS_CREATE_SID_FUNC() and - PS_VALIDATE_SID_FUNC() functions are defined. - . PS_FUNCS_SID() and PS_MOD_SID() have been removed. - Either use PS_FUNCS()/PS_MOD() or PS_FUNCS_UPDATE_TIMESTAMP()/ - PS_MOD_UPDATE_TIMESTAMP() if timestamp support exists. - -- ext/standard: - . _php_error_log() now has a formal return type of zend_result. - . _php_error_log() now accepts zend_string* values instead of char*. - . _php_error_log_ex() has been removed. - . php_mail()'s extra_cmd parameter is now a zend_string*. - . The php_math_round_mode_from_enum() function now takes a - zend_enum_RoundingMode parameter. - -- ext/uri: - . The value parameter of the php_uri_property_handler_write callback is now - const zval * instead of zval *, reflecting that write handlers must - not modify the input zval. - -- ext/xml: - . Removed the XML_ExpatVersion() libxml compatibility wrapper, - as it was unused. - . Removed the XML_GetCurrentByteCount() libxml compatibility wrapper, - as it was unused and could return the wrong result. - ======================== 4. OpCode changes ======================== -- Added ZEND_TYPE_ASSERT to check a value's type against the parameter - type of a function, throwing a TypeError on failure as if the function - was called. Used in optimizations that elide function calls. - ======================== 5. SAPI changes ======================== - -- SAPIs should explicitly release a thread's resources by calling - ts_free_thread() before terminating it. tsrm_shutdown() can only release the - resources of the calling thread, for resources allocated with - ts_allocate_tls_id(). - -- AG and SCNG are now allocated with ts_allocate_tls_id() and live in native - __thread storage on ZTS builds. - -- php-cli functionality is now available in embed builds via the do_php_cli() - function. diff --git a/Zend/zend.h b/Zend/zend.h index a0f094324426..6faab6353920 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -19,7 +19,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.6.0-dev" +#define ZEND_VERSION "4.7.0-dev" #define ZEND_ENGINE_3 diff --git a/Zend/zend_extensions.h b/Zend/zend_extensions.h index 1e6887e444a6..b8a384a1f544 100644 --- a/Zend/zend_extensions.h +++ b/Zend/zend_extensions.h @@ -43,7 +43,7 @@ You can use the following macro to check the extension API version for compatibi /* The first number is the engine version and the rest is the date (YYYYMMDD). * This way engine 2/3 API no. is always greater than engine 1 API no.. */ -#define ZEND_EXTENSION_API_NO 420250926 +#define ZEND_EXTENSION_API_NO 420260925 typedef struct _zend_extension_version_info { int zend_extension_api_no; diff --git a/Zend/zend_modules.h b/Zend/zend_modules.h index 3a98b1c06e29..bb3050927992 100644 --- a/Zend/zend_modules.h +++ b/Zend/zend_modules.h @@ -30,7 +30,7 @@ #define ZEND_MODULE_INFO_FUNC_ARGS zend_module_entry *zend_module #define ZEND_MODULE_INFO_FUNC_ARGS_PASSTHRU zend_module -#define ZEND_MODULE_API_NO 20250926 +#define ZEND_MODULE_API_NO 20260925 #ifdef ZTS #define USING_ZTS 1 #else diff --git a/Zend/zend_virtual_cwd.c b/Zend/zend_virtual_cwd.c index da27e9fd5426..da2f18367337 100644 --- a/Zend/zend_virtual_cwd.c +++ b/Zend/zend_virtual_cwd.c @@ -1027,6 +1027,19 @@ CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func fprintf(stderr,"cwd = %s path = %s\n", state->cwd, path); #endif +#ifdef ZEND_WIN32 + switch (php_win32_ioutil_path_kind_a(path, path_length)) { + case PHP_WIN32_IOUTIL_PATH_RESERVED: + SET_ERRNO_FROM_WIN32_CODE(ERROR_INVALID_NAME); + return 1; + case PHP_WIN32_IOUTIL_PATH_DEVICE: + memcpy(resolved_path, path, path_length + 1); + goto verify; + default: + break; + } +#endif + /* cwd_length can be 0 when getcwd() fails. * This can happen under solaris when a dir does not have read permissions * but *does* have execute permissions */ diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index e00aff17a924..674d1ed5b673 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -2522,7 +2522,7 @@ function gen_vm_opcodes_header( $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_HYBRID\n"; } if ($GLOBALS["vm_kind_name"][ZEND_VM_GEN_KIND] === "ZEND_VM_KIND_HYBRID" || $GLOBALS["vm_kind_name"][ZEND_VM_GEN_KIND] === "ZEND_VM_KIND_CALL") { - $str .= "#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__)) && defined(__clang__)\n"; + $str .= "#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__))\n"; $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_TAILCALL\n"; $str .= "#else\n"; $str .= "# define ZEND_VM_KIND\t\tZEND_VM_KIND_CALL\n"; diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h index 4e0d3ec43d9c..1d204b9281f5 100644 --- a/Zend/zend_vm_opcodes.h +++ b/Zend/zend_vm_opcodes.h @@ -41,7 +41,7 @@ static const char *const zend_vm_kind_name[] = { /* HYBRID requires support for computed GOTO and global register variables*/ #elif (defined(__GNUC__) && defined(HAVE_GCC_GLOBAL_REGS)) # define ZEND_VM_KIND ZEND_VM_KIND_HYBRID -#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__)) && defined(__clang__) +#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__)) # define ZEND_VM_KIND ZEND_VM_KIND_TAILCALL #else # define ZEND_VM_KIND ZEND_VM_KIND_CALL diff --git a/configure.ac b/configure.ac index b1ff51bcc2d1..5a62f91848f2 100644 --- a/configure.ac +++ b/configure.ac @@ -23,7 +23,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.6.0-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.7.0-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_MACRO_DIR([build]) AC_CONFIG_AUX_DIR([build]) diff --git a/docs/release-process.md b/docs/release-process.md index 2a53a00a2775..55bd45279b30 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -952,7 +952,6 @@ feature development that cannot go into the new version. `Zend/zend.h`, and `win32/build/confutils.js`; * update the API version numbers in `Zend/zend_extensions.h`, `Zend/zend_modules.h`, and `main/php.h`; and - * add the new branch to the list in `CONTRIBUTING.md`. See [Prepare for PHP 8.2][] and [Prepare for PHP 8.2 (bis)][] for an example of what this commit should include. diff --git a/ext/bz2/tests/gh20807.phpt b/ext/bz2/tests/gh20807.phpt index ee3238b711b9..ed52c7085858 100644 --- a/ext/bz2/tests/gh20807.phpt +++ b/ext/bz2/tests/gh20807.phpt @@ -12,6 +12,12 @@ if (PHP_OS === 'FreeBSD') die('skip Worker does not handle OOM gracefully'); if (PHP_OS_FAMILY === 'Darwin') die('skip Too slow'); if (PHP_INT_SIZE !== 8) die('skip Only for 64-bit systems'); if (getenv('SKIP_ASAN')) die('skip ASAN makes this test too slow'); +// The decompressed output needs more than 12 GiB of memory at its peak, which +// takes down smaller machines (e.g. 7 GB CI runners) with the OOM killer. +$memInfo = @file_get_contents('/proc/meminfo'); +if ($memInfo && preg_match('/MemAvailable:\s+(\d+) kB/', $memInfo, $m) && $m[1] < 13 * 1024 * 1024) { + die('skip Insufficient available memory (less than 13 GiB)'); +} ?> --FILE-- ucoll, static_cast(attribute), COLLATOR_ERROR_CODE_P( co ) ); COLLATOR_CHECK_STATUS( co, "Error getting attribute value" ); @@ -71,6 +75,10 @@ U_CFUNC PHP_FUNCTION( collator_set_attribute ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Set new value for the given attribute. */ ucol_setAttribute( co->ucoll, static_cast(attribute), static_cast(value), COLLATOR_ERROR_CODE_P( co ) ); COLLATOR_CHECK_STATUS( co, "Error setting attribute value" ); @@ -94,6 +102,10 @@ U_CFUNC PHP_FUNCTION( collator_get_strength ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Get current strength and return it. */ RETURN_LONG( ucol_getStrength( co->ucoll ) ); } @@ -116,6 +128,10 @@ U_CFUNC PHP_FUNCTION( collator_set_strength ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; + if (collator_check_initialized(co) == FAILURE) { + RETURN_THROWS(); + } + /* Set given strength. */ ucol_setStrength( co->ucoll, static_cast(strength) ); diff --git a/ext/intl/collator/collator_class.h b/ext/intl/collator/collator_class.h index 637d5dc490ae..c4be06fd1298 100644 --- a/ext/intl/collator/collator_class.h +++ b/ext/intl/collator/collator_class.h @@ -50,6 +50,21 @@ typedef struct { #define php_intl_collator_fetch_object(obj) ZEND_CONTAINER_OF(obj, Collator_object, zo) #define Z_INTL_COLLATOR_P(zv) php_intl_collator_fetch_object(Z_OBJ_P(zv)) +static zend_always_inline zend_result collator_check_initialized(Collator_object *co) +{ + ZEND_ASSERT(co != NULL); + + if (UNEXPECTED(co->ucoll == NULL)) { + intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); + intl_errors_set_custom_msg(COLLATOR_ERROR_P( co ), "Object not initialized"); + zend_throw_error(NULL, "Object not initialized"); + + return FAILURE; + } + + return SUCCESS; +} + #ifdef __cplusplus extern "C" { #endif diff --git a/ext/intl/collator/collator_compare.cpp b/ext/intl/collator/collator_compare.cpp index bac0bbf50b21..4bfe0956a3f7 100644 --- a/ext/intl/collator/collator_compare.cpp +++ b/ext/intl/collator/collator_compare.cpp @@ -55,11 +55,7 @@ U_CFUNC PHP_FUNCTION( collator_compare ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg(COLLATOR_ERROR_P( co ), "Object not initialized"); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/collator/collator_locale.cpp b/ext/intl/collator/collator_locale.cpp index ea1393779846..20aba988fb40 100644 --- a/ext/intl/collator/collator_locale.cpp +++ b/ext/intl/collator/collator_locale.cpp @@ -48,11 +48,7 @@ U_CFUNC PHP_FUNCTION( collator_get_locale ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( nullptr, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), "Object not initialized"); - zend_throw_error(nullptr, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/collator/collator_sort.cpp b/ext/intl/collator/collator_sort.cpp index 3f2e1cf543d8..dc5898d23a6e 100644 --- a/ext/intl/collator/collator_sort.cpp +++ b/ext/intl/collator/collator_sort.cpp @@ -433,11 +433,7 @@ U_CFUNC PHP_FUNCTION( collator_sort_with_sort_keys ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( nullptr, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), "Object not initialized"); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } @@ -598,11 +594,7 @@ U_CFUNC PHP_FUNCTION( collator_get_sort_key ) /* Fetch the object. */ COLLATOR_METHOD_FETCH_OBJECT; - if (!co || !co->ucoll) { - intl_error_set_code( nullptr, COLLATOR_ERROR_CODE( co ) ); - intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), "Object not initialized"); - zend_throw_error(NULL, "Object not initialized"); - + if (collator_check_initialized(co) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/intl/tests/collator_attribute_unconstructed.phpt b/ext/intl/tests/collator_attribute_unconstructed.phpt new file mode 100644 index 000000000000..f8eb5afa01d5 --- /dev/null +++ b/ext/intl/tests/collator_attribute_unconstructed.phpt @@ -0,0 +1,55 @@ +--TEST-- +Collator attribute and strength methods on unconstructed object +--EXTENSIONS-- +intl +--FILE-- + fn() => $c->getAttribute(Collator::NUMERIC_COLLATION), + 'setAttribute' => fn() => $c->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON), + 'getStrength' => fn() => $c->getStrength(), + 'setStrength' => fn() => $c->setStrength(Collator::SECONDARY), +]; + +foreach ($methods as $method => $call) { + try { + $call(); + } catch (Error $e) { + echo $method, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +$functions = [ + 'collator_get_attribute' => fn() => collator_get_attribute($c, Collator::NUMERIC_COLLATION), + 'collator_set_attribute' => fn() => collator_set_attribute($c, Collator::NUMERIC_COLLATION, Collator::ON), + 'collator_get_strength' => fn() => collator_get_strength($c), + 'collator_set_strength' => fn() => collator_set_strength($c, Collator::SECONDARY), +]; + +foreach ($functions as $function => $call) { + try { + $call(); + } catch (Error $e) { + echo $function, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +?> +--EXPECT-- +getAttribute: Error: Object not initialized +setAttribute: Error: Object not initialized +getStrength: Error: Object not initialized +setStrength: Error: Object not initialized +collator_get_attribute: Error: Object not initialized +collator_set_attribute: Error: Object not initialized +collator_get_strength: Error: Object not initialized +collator_set_strength: Error: Object not initialized diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index dad8bc52ddd1..238c5db31daf 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -168,11 +168,11 @@ class my_mysqli_fake_packet_generator return new my_mysqli_fake_packet_item(null, $packed_value, $is_hex); } - public function server_ok(): my_mysqli_fake_packet + public function server_ok($number = "02"): my_mysqli_fake_packet { $packet = new my_mysqli_fake_packet(); $packet->packet_length = "070000"; - $packet->packet_number = "02"; + $packet->packet_number = $number; $packet->header = "00"; // OK $packet->affected_rows = "00"; $packet->last_insert_id = "00"; @@ -181,6 +181,17 @@ class my_mysqli_fake_packet_generator return $packet; } + public function server_eof(): my_mysqli_fake_packet + { + $packet = new my_mysqli_fake_packet(); + $packet->packet_length = "050000"; + $packet->packet_number = "01"; + $packet->header = "fe"; // EOF + $packet->warning_count = "0000"; + $packet->server_status = "0200"; + return $packet; + } + public function server_greetings(): my_mysqli_fake_packet { $packet = new my_mysqli_fake_packet(); @@ -204,6 +215,25 @@ class my_mysqli_fake_packet_generator return $packet; } + public function server_greetings_sha256(): my_mysqli_fake_packet + { + $packet = $this->server_greetings(); + // 6 bytes shorter than the default mysql_native_password greeting + $packet->packet_length = "520000"; + $packet->mariadb_extended_server_capabilities_auth_plugin = + self::create_packet_item('sha256_password'); + + return $packet; + } + public function server_greetings_caching_sha2(): my_mysqli_fake_packet + { + $packet = $this->server_greetings(); + // same length as the default mysql_native_password so the length stays + $packet->mariadb_extended_server_capabilities_auth_plugin = + self::create_packet_item('caching_sha2_password'); + + return $packet; + } public function server_tabular_query_response(): array { $qr1 = new my_mysqli_fake_packet(); @@ -246,7 +276,6 @@ class my_mysqli_fake_packet_generator $qr1->packet_number = "01"; $qr1->field_count = "00"; // UPSERT $qr1->affected_rows = "00"; - $qr1->affected_rows = "00"; $qr1->last_insert_id = "00"; $qr1->server_status = "0000"; $qr1->warning_count = "0000"; @@ -257,6 +286,21 @@ class my_mysqli_fake_packet_generator return [$qr1]; } + public function server_upsert_result_response(): my_mysqli_fake_packet + { + $ur = new my_mysqli_fake_packet(); + $ur->packet_length = "300000"; + $ur->packet_number = "01"; + $ur->field_count = "00"; // UPSERT + $ur->affected_rows = "00"; + $ur->last_insert_id = "00"; + $ur->server_status = "0002"; + $ur->warning_count = "0000"; + $ur->payload = "28526f7773206d6174636865643a203120204368616e6765643a203020205761726e696e67733a2030"; + + return $ur; + } + public function server_stmt_prepare_response_start($num_field): my_mysqli_fake_packet { $pr1 = new my_mysqli_fake_packet(); @@ -738,6 +782,44 @@ function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server $conn->read(65536); } +function my_mysqli_test_stmt_response_row_short_length( + my_mysqli_fake_server_conn $conn, + string $field_name +): void { + $rh = $conn->packet_generator->server_stmt_execute_data_response($field_name); + + // The length does not cover the fixed offsets that the field type reads + $rh[4]->packet_length = '090000'; + $rh[4]->row_field2 = '01de'; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send_server_stmt_prepare_data_response($field_name); + $conn->read(); + $conn->send( + $conn->packets_to_bytes($rh), + "Malicious Stmt Response for data $field_name [length too short]" + ); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_row_short_length_date(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'datval'); +} + +function my_mysqli_test_stmt_response_row_short_length_time(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'timval'); +} + +function my_mysqli_test_stmt_response_row_short_length_datetime(my_mysqli_fake_server_conn $conn): void +{ + my_mysqli_test_stmt_response_row_short_length($conn, 'dtival'); +} + function my_mysqli_test_stmt_response_row_over_read_two_fields( my_mysqli_fake_server_conn $conn, string $field_name, @@ -831,6 +913,114 @@ function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server $conn->read(65536); } +function my_mysqli_test_query_response_row_field_len(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + + // 2-byte length prefix (0xfc) with the packet ending right after it, so + // decoding the length itself would read past the end of the packet + $rh[4]->row_field2 = 'fc'; + $rh[4]->packet_length = '060000'; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [field length over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row(my_mysqli_fake_server_conn $conn, string $packet_length, + string $row_field2, string $message): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + $rh[4]->packet_length = $packet_length; + $rh[4]->row_field2 = $row_field2; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), $message); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row_eof_short(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + // EOF row packet that has no space for the warnings and the status + $rh[5]->packet_length = '030000'; + $rh[5]->server_status = ''; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response [short EOF row packet]"); + $conn->read(65536); +} + +function my_mysqli_test_query_response_row_field_null(my_mysqli_fake_server_conn $conn): void +{ + // NULL field, which is encoded as a single 0xfb byte + my_mysqli_test_query_response_row($conn, "060000", "fb", "Query Response for data strval field [NULL]"); +} + +function my_mysqli_test_query_response_row_field_len_2_bytes(my_mysqli_fake_server_conn $conn): void +{ + // field length encoded on 2 bytes + my_mysqli_test_query_response_row($conn, "0c0000", "fc040074657374", + "Query Response for data strval field [length on 2 bytes]"); +} + +function my_mysqli_test_query_rset_header_field_over_read(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + + $rh[0]->num_fields = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [length overflow]"); + $conn->read(65536); +} + +function my_mysqli_test_upsert_rset_header_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $ur = $conn->packet_generator->server_upsert_result_response(); + + $ur->packet_length = "020000"; + $ur->affected_rows = "fd"; + $ur->last_insert_id = ""; + $ur->server_status = ""; + $ur->warning_count = ""; + $ur->payload = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($ur->to_bytes(), "Malicious Upsert Response [affected rows overflow]"); + $conn->read(65536); +} + +function my_mysqli_test_upsert_rset_header_packet_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $ur = $conn->packet_generator->server_upsert_result_response(); + + $ur->affected_rows = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($ur->to_bytes(), "Malicious Upsert Response [affected rows overflow]"); + $conn->read(65536); +} + + function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void { $conn->send_server_greetings(); @@ -844,7 +1034,401 @@ function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server } } -function run_fake_server(string $test_function, int|string $port = 0): int +function my_mysqli_test_auth_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "020000"; + $p->affected_rows = "fe"; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_auth_last_insert_id_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "030000"; + $p->last_insert_id = "fd"; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_auth_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "060000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_cached_sha2_result(my_mysqli_fake_server_conn $conn, string $packet_length, string $payload, + string $message): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = $packet_length; + $p->packet_number = "02"; + if ($payload !== '') { + $p->payload = $payload; + } + + $conn->send($conn->packet_generator->server_greetings_caching_sha2()->to_bytes(), "Server Greeting"); + $conn->read(65536); + // sent in one go so that the client cannot interleave its output in between + $conn->send($p->to_bytes() . $conn->packet_generator->server_ok("03")->to_bytes(), $message); + $conn->read(65536); +} + +function my_mysqli_test_cached_sha2_result_fast_auth(my_mysqli_fake_server_conn $conn): void +{ + // What a real server sends for a successful fast auth: CR_OK and the status byte + my_mysqli_test_cached_sha2_result($conn, "020000", "0103", "Cached SHA2 Result [fast auth]"); +} + +function my_mysqli_test_cached_sha2_result_empty(my_mysqli_fake_server_conn $conn): void +{ + // Empty packet, so reading the response code reads past the packet + my_mysqli_test_cached_sha2_result($conn, "000000", "", "Malicious Cached SHA2 Result [empty packet]"); +} + +function my_mysqli_test_cached_sha2_result_len(my_mysqli_fake_server_conn $conn): void +{ + // Only the response code is left, so there is no space for the status byte + my_mysqli_test_cached_sha2_result($conn, "010000", "01", "Malicious Cached SHA2 Result [packet too short]"); +} + +function my_mysqli_test_sha256_pk_response_empty(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + // Empty packet, so reading the response code reads past the packet + $p->packet_length = "000000"; + $p->packet_number = "02"; + + $conn->send($conn->packet_generator->server_greetings_sha256()->to_bytes(), "Server Greeting"); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious SHA256 PK Response [empty packet]"); + $conn->read(65536); + $conn->send($conn->packet_generator->server_ok("04")->to_bytes(), "Server OK"); + $conn->read(65536); +} + +function my_mysqli_test_sha256_pk_response_len(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + // Only the response code is left, so there is no space for the public key + $p->packet_length = "010000"; + $p->packet_number = "02"; + $p->response_code = "00"; + + $conn->send($conn->packet_generator->server_greetings_sha256()->to_bytes(), "Server Greeting"); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious SHA256 PK Response [packet too short]"); + $conn->read(65536); + $conn->send($conn->packet_generator->server_ok("04")->to_bytes(), "Server OK"); + $conn->read(65536); +} + +/* An auth switch packet whose plugin name is not NUL terminated. */ +function my_mysqli_auth_switch_packet(string $packet_number): my_mysqli_fake_packet +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "160000"; + $p->packet_number = $packet_number; + $p->response_code = "fe"; + // 21 bytes of plugin name and no terminator + $p->auth_plugin_name = bin2hex('mysql_native_password'); + + return $p; +} + +function my_mysqli_test_auth_response_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send(my_mysqli_auth_switch_packet("02")->to_bytes(), + "Malicious Auth Switch Response [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send(my_mysqli_auth_switch_packet("01")->to_bytes(), + "Malicious Change User Response [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_cached_sha2_switch_unterminated(my_mysqli_fake_server_conn $conn): void +{ + $conn->send($conn->packet_generator->server_greetings_caching_sha2()->to_bytes(), "Server Greeting"); + $conn->read(65536); + // sent in one go so that the client cannot interleave its output in between + $conn->send(my_mysqli_auth_switch_packet("02")->to_bytes() + . $conn->packet_generator->server_ok("03")->to_bytes(), + "Malicious Cached SHA2 Result [plugin name not terminated]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_response_empty(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + // Empty packet, so reading the response code reads past the packet + $p->packet_length = "000000"; + $p->header = ""; + $p->affected_rows = ""; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious Change User Response [empty packet]"); + $conn->read(65536); +} + +function my_mysqli_test_chg_user_response_len(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + // Only the response code is left, so there is no space for the rest + $p->packet_length = "010000"; + $p->affected_rows = ""; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(65536); + $conn->send($p->to_bytes(), "Malicious Change User Response [packet too short]"); + $conn->read(65536); +} + +function my_mysqli_test_auth_ok(my_mysqli_fake_server_conn $conn, string $packet_length, string $affected_rows, + string $last_insert_id, string $server_status, string $warning_count, + string $message): void +{ + $p = $conn->packet_generator->server_ok(); + $p->packet_length = $packet_length; + $p->affected_rows = $affected_rows; + $p->last_insert_id = $last_insert_id; + $p->server_status = $server_status; + $p->warning_count = $warning_count; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), $message); + $conn->read(); +} + +function my_mysqli_test_auth_last_insert_id_no_space(my_mysqli_fake_server_conn $conn): void +{ + // affected rows takes the last byte, so there is nothing left for the last insert id + my_mysqli_test_auth_ok($conn, "020000", "00", "", "", "", + "Malicious OK Auth Response [no space for last insert id]"); +} + +function my_mysqli_test_auth_affected_rows_null_length(my_mysqli_fake_server_conn $conn): void +{ + // affected rows sent as the NULL length marker + my_mysqli_test_auth_ok($conn, "070000", "fb", "00", "0200", "0000", + "OK Auth Response [affected rows as NULL length]"); +} + +function my_mysqli_test_auth_affected_rows_2_bytes(my_mysqli_fake_server_conn $conn): void +{ + // affected rows encoded on 2 bytes + my_mysqli_test_auth_ok($conn, "090000", "fc3412", "00", "0200", "0000", + "OK Auth Response [affected rows on 2 bytes]"); +} + +function my_mysqli_test_auth_affected_rows_8_bytes(my_mysqli_fake_server_conn $conn): void +{ + // affected rows encoded on 8 bytes + my_mysqli_test_auth_ok($conn, "0f0000", "fe0100000000000000", "00", "0200", "0000", + "OK Auth Response [affected rows on 8 bytes]"); +} + +function my_mysqli_test_greet_scramble_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_greetings(); + // the scramble length claims more data than the packet holds + $p->auth_plugin = "ff"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting [scramble length over-read]"); + $conn->read(); +} + +function my_mysqli_test_greet_over_read_string(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "080000"; + $p->packet_number = "00"; + $p->proto_version = "0a"; + $p->version = my_mysqli_fake_packet_generator::create_packet_item('5.5'); + $p->thread_id = "03030303"; + $p->salt = "473e3f6047257c67"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting"); + $conn->read(); +} + +function my_mysqli_test_greet_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "110000"; + $p->packet_number = "00"; + $p->proto_version = "0a"; + $p->version = my_mysqli_fake_packet_generator::create_packet_item('8.0' . chr(0)); + $p->thread_id = "03000000"; + $p->salt = "473e3f6047257c67"; + + $conn->send($p->to_bytes(), "Malicious Server Greeting"); + $conn->read(); +} + +function my_mysqli_test_ok_affected_rows_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "020000"; + $p->affected_rows = "fe"; + $p->last_insert_id = ""; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_last_insert_id_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "030000"; + $p->last_insert_id = "fd"; + $p->server_status = ""; + $p->warning_count = ""; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_message_len_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "080000"; + $p->message_len = "fd"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_ok_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_ok("01"); + $p->packet_length = "060000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious OK Auth Response"); + $conn->read(); +} + +function my_mysqli_test_eof_warning_count_over_read(my_mysqli_fake_server_conn $conn): void +{ + $p = $conn->packet_generator->server_eof(); + $p->packet_length = "040000"; + $p->warning_count = "00"; // Shrunk 1 byte + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($p->to_bytes(), "Malicious EOF Response"); + $conn->read(); +} + +function my_mysqli_test_stmt_response_row_over_read_status(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_stmt_execute_items_response(); + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + $packets[2]->server_status = '02'; + $packets[2]->packet_length = '040000'; + $conn->send($conn->packets_to_bytes($packets), "Stmt prepare items"); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for items [Extract heap through buffer over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_field_len_faulty(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_stmt_execute_items_response(); + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + // org_name length exceeds the whole remaining packet + $packets[1]->orig_name_len = 'fe'; + $packets[2]->packet_length = '1f0000'; + $conn->send($conn->packets_to_bytes($packets), "Stmt prepare items"); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for fields [Extract heap through buffer over-read]"); + $conn->read(65536); +} + +function my_mysqli_test_stmt_response_field_len_premature(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $packets = $conn->packet_generator->server_stmt_prepare_items_response(); + // org_name length over-reads into the trailing fixed-length metadata block + $packets[1]->orig_name_len = '0c'; + $conn->send($conn->packets_to_bytes($packets), "Malicious Stmt Prepare items [Field length over-read]"); + $conn->read(65536); +} + +function run_fake_server(string $test_function, int|string $port = 0): void { $host = '127.0.0.1'; @@ -873,9 +1457,12 @@ function run_fake_server(string $test_function, int|string $port = 0): int echo "[*] Server finished\n"; } - function run_fake_server_in_background($test_function, $port = 0): my_mysqli_fake_server_process { + if ($port == 0) { + $port = (int) getenv('MYSQLI_TEST_FAKE_SERVER_PORT'); + } + $command = [PHP_BINARY, '-n', __FILE__, 'mysqli_fake_server', $test_function, $port]; $descriptorspec = array( diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt new file mode 100644 index 000000000000..45d33cdf7fc1 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-2-bytes.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows on 2 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows on 2 bytes]: 0900000200fc34120002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt new file mode 100644 index 000000000000..63233ad8291e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-8-bytes.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows on 8 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows on 8 bytes]: 0f00000200fe01000000000000000002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt new file mode 100644 index 000000000000..6a18f36d37c6 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows-null-length.phpt @@ -0,0 +1,32 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with affected rows as NULL length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - OK Auth Response [affected rows as NULL length]: 0700000200fb0002000000 +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt new file mode 100644 index 000000000000..13856f058f4e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-affected-rows.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in affected rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 0200000200fe + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt new file mode 100644 index 000000000000..3a163ef63a5b --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id-no-space.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet with no space for last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response [no space for last insert id]: 020000020000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt new file mode 100644 index 000000000000..9d30f04dfc98 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-last-insert-id.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 030000020000fd + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt new file mode 100644 index 000000000000..a4f5ca20a0c2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-packet-warning-count.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious OK Auth Response: 06000002000000020000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt new file mode 100644 index 000000000000..8c1415a7bd0b --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-auth-switch-unterminated.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - auth switch response with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 7d00000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400140c7b6398a9794c7dc95737fa731fe62e95fe56626d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Auth Switch Response [plugin name not terminated]: 16000002fe6d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): AUTH_RESPONSE packet shorter than expected in %s on line %d +mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt new file mode 100644 index 000000000000..9a7c96116e78 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-empty.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result with empty packet) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [empty packet]: 000000020700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt new file mode 100644 index 000000000000..568f1f0b5dbb --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-fast-auth.phpt @@ -0,0 +1,41 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result for a successful fast auth) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Cached SHA2 Result [fast auth]: 0200000201030700000300000002000000 +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt new file mode 100644 index 000000000000..77b19832cc02 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-len.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result shorter than expected) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [packet too short]: 01000002010700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt new file mode 100644 index 000000000000..2554a46c5a00 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-cached-sha2-switch-unterminated.phpt @@ -0,0 +1,40 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - cached sha2 result with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +echo "[*] connect_errno: ", $conn->connect_errno, "\n"; +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c64310063616368696e675f736861325f70617373776f7264 +[*] Received: 8900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400204829cef922c9e8d5c6b4a4299cb857d65b18323a1b833559f3aa6a0b462994be63616368696e675f736861325f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Cached SHA2 Result [plugin name not terminated]: 16000002fe6d7973716c5f6e61746976655f70617373776f72640700000300000002000000 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d +[*] connect_errno: 0 +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt new file mode 100644 index 000000000000..66bde99bef01 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-empty.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response with empty packet) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Changing user on the fake server...\n"; +var_dump($conn->change_user("root2", "", "")); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [empty packet]: 00000001 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +bool(false) +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt new file mode 100644 index 000000000000..5a2b2c6b1baa --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-len.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response shorter than expected) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Changing user on the fake server...\n"; +var_dump($conn->change_user("root2", "", "")); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [packet too short]: 0100000100 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +bool(false) +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt new file mode 100644 index 000000000000..e6403f5f89a8 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-chg-user-switch-unterminated.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - change user response with an unterminated plugin name) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, "", "", $process->getPort() ); + +echo "[*] Changing user on the fake server...\n"; +try { + var_dump($conn->change_user("root2", "", "")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Changing user on the fake server... +[*] Received: 4e00000011726f6f743200000008006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Malicious Change User Response [plugin name not terminated]: 16000001fe6d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::change_user(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::change_user(): CHANGE_USER packet shorter than expected in %s on line %d +mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt new file mode 100644 index 000000000000..2f1f0f054d92 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-eof-warning-count.phpt @@ -0,0 +1,44 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - eof packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + $query = "SELECT 1; SELECT 2;"; + $conn->multi_query($query); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 030000001b0000 +[*] Sending - Malicious EOF Response: 04000001fe000200 + +Warning: mysqli::multi_query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::multi_query(): EOF packet shorter than expected in %s on line %d + +Warning: mysqli::multi_query(): Error while reading SET_OPTION's response packet. PID=%d in %s on line %d +[*] Received: 140000000353454c45435420313b2053454c45435420323b +MySQL server has gone away +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt new file mode 100644 index 000000000000..210ffb8f8702 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet-string.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet over-read string) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting: 080000000a352e3503030303473e3f6047257c67 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt new file mode 100644 index 000000000000..7bcbd264dec0 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-packet.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet over-read) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting: 110000000a382e300003000000473e3f6047257c67 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt new file mode 100644 index 000000000000..c85401d9a4b2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-greet-scramble-len.phpt @@ -0,0 +1,37 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - greet packet scramble length over-read) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Malicious Server Greeting [scramble length over-read]: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81ff0000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): GREET packet shorter than expected in %s on line %d + +Warning: mysqli::__construct(): Error while reading greeting packet. PID=%d in %s on line %d +Unknown error while trying to connect via tcp://127.0.0.1:%d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt new file mode 100644 index 000000000000..953abde7d6e6 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-affected-rows.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in affected rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 0200000100fe + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt new file mode 100644 index 000000000000..fe2f0c7757bf --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-last-insert-id.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in last inserted id) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 030000010000fd + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt new file mode 100644 index 000000000000..ed332988994a --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-message-len.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in message length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 0800000100000002000000fd + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt new file mode 100644 index 000000000000..70ce5b8debb2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-ok-packet-warning-count.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - ok packet over-read in warning count) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + $conn = new mysqli($servername, $username, $password, "", $process->getPort()); + var_dump($conn->select_db("php_test")); +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 09000000027068705f74657374 +[*] Sending - Malicious OK Auth Response: 06000001000000020000 + +Warning: mysqli::select_db(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::select_db(): OK packet shorter than expected in %s on line %d + +Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d +Malformed packet +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt new file mode 100644 index 000000000000..685af9948ba3 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-eof-short.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - row packet with an EOF shorter than the status it announces) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response [short EOF row packet]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374047465737403000006fe0000 +array(2) { + [0]=> + string(4) "test" + [1]=> + string(4) "test" +} +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt new file mode 100644 index 000000000000..dd7a496b26ea --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len-2-bytes.phpt @@ -0,0 +1,49 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row field length on 2 bytes) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Query Response for data strval field [length on 2 bytes]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000c0000050474657374fc04007465737405000006fe00002200 +array(2) { + [0]=> + string(4) "test" + [1]=> + string(4) "test" +} +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt new file mode 100644 index 000000000000..1b1d80c400e2 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-len.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row field length prefix) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$sql = "SELECT strval, strval FROM data"; + +$result = $conn->query($sql); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + var_dump($row['strval']); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response for data strval field [field length over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00002200060000050474657374fc05000006fe00002200 + +Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after end of packet in %s on line %d +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt new file mode 100644 index 000000000000..dc6e6703374d --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-query-row-field-null.phpt @@ -0,0 +1,49 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - text protocol row with a NULL field) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +if ($result->num_rows > 0) { + while ($row = $result->fetch_row()) { + var_dump($row); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Query Response for data strval field [NULL]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00002200060000050474657374fb05000006fe00002200 +array(2) { + [0]=> + string(4) "test" + [1]=> + NULL +} +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt new file mode 100644 index 000000000000..066844195144 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-field-len.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header read field length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$sql = "SELECT strval, strval FROM data"; + +$result = $conn->query($sql); + +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + var_dump($row['strval']); + } +} +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Query the fake server... +[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 +[*] Sending - Malicious Query Response for data strval field [length overflow]: 01000001fd3200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374047465737405000006fe00002200 + +Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::query(): RSET_HEADER packet shorter than expected in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt new file mode 100644 index 000000000000..7e91c0db5f53 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-affected-rows.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header upsert affected_rows) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +// Construct the SQL query directly +$sql = "UPDATE data SET strval = 'test' WHERE id = 1"; + +// Execute the query +$result = $conn->query($sql); +if ($result) { + echo "Affected rows: " . $conn->affected_rows . "\n"; + echo "Info: " . $conn->info . "\n"; +} + +// Close the connection +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 2d000000035550444154452064617461205345542073747276616c203d20277465737427205748455245206964203d2031 +[*] Sending - Malicious Upsert Response [affected rows overflow]: 0200000100fd + +Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::query(): RSET_HEADER packet shorter than expected in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt new file mode 100644 index 000000000000..13193abaab08 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-rset-header-upsert-len.phpt @@ -0,0 +1,48 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - rset header upsert packet length) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +// Construct the SQL query directly +$sql = "UPDATE data SET strval = 'test' WHERE id = 1"; + +// Execute the query +$result = $conn->query($sql); +if ($result) { + echo "Affected rows: " . $conn->affected_rows . "\n"; + echo "Info: " . $conn->info . "\n"; +} + +// Close the connection +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Received: 2d000000035550444154452064617461205345542073747276616c203d20277465737427205748455245206964203d2031 +[*] Sending - Malicious Upsert Response [affected rows overflow]: 3000000100fd000002000028526f7773206d6174636865643a203120204368616e6765643a203020205761726e696e67733a2030 + +Warning: mysqli::query(): RSET_HEADER packet additional data length is past 82 bytes the packet size in %s on line %d + +Warning: mysqli::query(): Error reading result set's header in %s on line %d +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt new file mode 100644 index 000000000000..14357a39613c --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-empty.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - sha256 public key response with empty packet) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 520000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431007368613235365f70617373776f7264 +[*] Received: 0100000101 +[*] Sending - Malicious SHA256 PK Response [empty packet]: 00000002 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d + +Warning: Error while receiving public key. PID=%d in %s on line %d +[*] Received: 6300000385a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400007368613235365f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000400000002000000 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt new file mode 100644 index 000000000000..d56e69b816c5 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-sha256-pk-len.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - sha256 public key response shorter than expected) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +echo "[*] Connecting to the fake server...\n"; +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +$conn->close(); + +$process->terminate(true); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connecting to the fake server... +[*] Connection established +[*] Sending - Server Greeting: 520000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431007368613235365f70617373776f7264 +[*] Received: 0100000101 +[*] Sending - Malicious SHA256 PK Response [packet too short]: 0100000200 + +Warning: mysqli::__construct(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::__construct(): SHA256_PK_REQUEST_RESPONSE packet shorter than expected in %s on line %d + +Warning: Error while receiving public key. PID=%d in %s on line %d +[*] Received: 6300000385a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400007368613235365f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000400000002000000 +[*] Received: 0100000001 +[*] Server finished +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt new file mode 100644 index 000000000000..44d61c244149 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-faulty.phpt @@ -0,0 +1,38 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt field length exceeding the packet) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Stmt prepare items: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656dfe6974656d0ce000c8000000fd01100000001f000003fe00000200 + +Warning: mysqli::prepare(): Protocol error. Server sent NULL_LENGTH. The server is faulty in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt new file mode 100644 index 000000000000..69756273b9a8 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-field-len-premature.phpt @@ -0,0 +1,42 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt field length over-reading into fixed block) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Malicious Stmt Prepare items [Field length over-read]: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656d0c6974656d0ce000c8000000fd011000000005000003fe00000200 + +Warning: mysqli::prepare(): Protocol error. Server sent false length. Expected 12 in %s on line %d + +Warning: mysqli::prepare(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::prepare(): Result set field packet shorter than expected in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt new file mode 100644 index 000000000000..1c71da1cbbc4 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-response-row-status.phpt @@ -0,0 +1,40 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt response) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli( $servername, $username, $password, "", $process->getPort() ); + +echo "[*] Preparing statement on the fake server...\n"; +$conn->prepare("SELECT item FROM items"); + +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Preparing statement on the fake server... +[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 +[*] Sending - Stmt prepare items: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656d046974656d0ce000c8000000fd011000000004000003fe000002 + +Warning: mysqli::prepare(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d + +Warning: mysqli::prepare(): EOF packet shorter than expected in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt new file mode 100644 index 000000000000..e01dc3ff5c66 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-date.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row date field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, datval FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2064617476616c2046524f4d2064617461 +[*] Sending - Stmt prepare data datval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data datval [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt new file mode 100644 index 000000000000..8e86d4039879 --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-datetime.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row datetime field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, dtival FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2064746976616c2046524f4d2064617461 +[*] Sending - Stmt prepare data dtival: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data dtival [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt new file mode 100644 index 000000000000..0b9335c42f4e --- /dev/null +++ b/ext/mysqli/tests/ghsa-r6x9-5r99-36j7-stmt-row-short-length-time.phpt @@ -0,0 +1,45 @@ +--TEST-- +GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd_writeprotocol.c - stmt row time field length too short for the type) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, timval FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c2074696d76616c2046524f4d2064617461 +[*] Sending - Stmt prepare data timval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data timval [length too short]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00002200090000050000047465737401de05000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. Field length is too short for the field type in %s on line %d +done! diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index fc3c095f9cec..b961c0436c92 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -346,6 +346,9 @@ mysqlnd_stmt_prepare_read_eof(MYSQLND_STMT * s) if (FAIL == (ret = PACKET_READ(conn, &fields_eof))) { if (stmt->result) { stmt->result->m.free_result_contents(stmt->result); + /* The memset() below resets the statement, so release what it still owns first. */ + conn->m->free_reference(conn); + mnd_efree(stmt->execute_cmd_buffer.buffer); /* XXX: This will crash, because we will null also the methods. But seems it happens in extreme cases or doesn't. Should be fixed by exporting a function (from mysqlnd_driver.c?) to do the reset. diff --git a/ext/mysqlnd/mysqlnd_ps_codec.c b/ext/mysqlnd/mysqlnd_ps_codec.c index 428b60b810ad..45dfa0e06633 100644 --- a/ext/mysqlnd/mysqlnd_ps_codec.c +++ b/ext/mysqlnd/mysqlnd_ps_codec.c @@ -59,7 +59,9 @@ static inline bool ps_fetch_is_packet_over_read_with_variable_length(const unsig return false; } size_t length_len = *row - p; - if (length_len > pack_len || length > pack_len - length_len) { + /* This assert should never fire, otherwise we invoked UB earlier */ + ZEND_ASSERT(length_len <= pack_len); + if (length > pack_len - length_len) { ps_fetch_over_read_error(row); return true; } @@ -76,6 +78,18 @@ static inline bool ps_fetch_is_packet_over_read_with_static_length(const unsigne return false; } +/* The declared length has to cover the fixed offsets that the field type reads. */ +static inline bool ps_fetch_is_length_too_short(const zend_uchar ** row, const zend_ulong length, + const unsigned int min_length) +{ + if (UNEXPECTED(length < min_length)) { + php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length is too short for the field type"); + *row = NULL; + return true; + } + return false; +} + /* {{{ ps_fetch_from_1_to_8_bytes */ void @@ -255,11 +269,15 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p const zend_uchar *p = *row; DBG_ENTER("ps_fetch_time"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 8))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_TIME; @@ -269,7 +287,7 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.hour = (unsigned int) to[5]; t.minute = (unsigned int) to[6]; t.second = (unsigned int) to[7]; - t.second_part = (length > 8) ? (zend_ulong) sint4korr(to+8) : 0; + t.second_part = (length >= 12) ? (zend_ulong) sint4korr(to+8) : 0; t.year = t.month= 0; if (t.day) { /* Convert days to hours at once */ @@ -305,11 +323,15 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p const zend_uchar *p = *row; DBG_ENTER("ps_fetch_date"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -321,7 +343,7 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -342,11 +364,15 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i const zend_uchar *p = *row; DBG_ENTER("ps_fetch_datetime"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -356,16 +382,16 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - if (length > 4) { + if (length >= 7) { t.hour = (unsigned int) to[4]; t.minute = (unsigned int) to[5]; t.second = (unsigned int) to[6]; } else { t.hour = t.minute = t.second= 0; } - t.second_part = (length > 7) ? (zend_ulong) sint4korr(to+7) : 0; + t.second_part = (length >= 11) ? (zend_ulong) sint4korr(to+7) : 0; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -389,7 +415,7 @@ static void ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) { const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } @@ -409,7 +435,7 @@ static void ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) { const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index c779f747ec8c..65b0bceae591 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -22,11 +22,18 @@ #include "mysqlnd_statistics.h" #include "mysqlnd_debug.h" -#define BAIL_IF_NO_MORE_DATA \ - if (UNEXPECTED((size_t)(p - begin) > packet->header.size)) { \ +#define BAIL_PREMATURE_END do { \ php_error_docref(NULL, E_WARNING, "Premature end of data (mysqlnd_wireprotocol.c:%u)", __LINE__); \ goto premature_end; \ - } \ + } while (0) + +/* Bail out unless the packet still has the required number of bytes left to read. */ +#define BAIL_IF_NOT_ENOUGH_DATA_EX(min_required_bytes) \ + if (UNEXPECTED((size_t)(p - begin) + (min_required_bytes) > packet->header.size)) { \ + BAIL_PREMATURE_END; \ + } + +#define BAIL_IF_NOT_ENOUGH_DATA BAIL_IF_NOT_ENOUGH_DATA_EX(1) static const char *unknown_sqlstate= "HY000"; @@ -89,10 +96,14 @@ static enum_mysqlnd_collected_stats packet_type_to_statistic_packet_count[PROT_L /* {{{ php_mysqlnd_net_field_length Get next field's length */ zend_ulong -php_mysqlnd_net_field_length(const zend_uchar **packet) +php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size) { const zend_uchar *p= (const zend_uchar *)*packet; + if (UNEXPECTED(remaining_size == 0)) { + return MYSQLND_INVALID_NET_FIELD_LENGTH; + } + if (*p < 251) { (*packet)++; return (zend_ulong) *p; @@ -103,14 +114,23 @@ php_mysqlnd_net_field_length(const zend_uchar **packet) (*packet)++; return MYSQLND_NULL_LENGTH; case 252: - (*packet) += 3; - return (zend_ulong) uint2korr(p+1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + return (zend_ulong) uint2korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; case 253: - (*packet) += 4; - return (zend_ulong) uint3korr(p+1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + return (zend_ulong) uint3korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; default: - (*packet) += 9; - return (zend_ulong) uint4korr(p+1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + return (zend_ulong) uint4korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; } } /* }}} */ @@ -118,29 +138,53 @@ php_mysqlnd_net_field_length(const zend_uchar **packet) /* {{{ php_mysqlnd_net_field_length_ll Get next field's length */ -uint64_t -php_mysqlnd_net_field_length_ll(const zend_uchar **packet) +MYSQLND_OPTIONAL_UINT64_T +php_mysqlnd_net_field_length_ll(const zend_uchar **packet, size_t remaining_size) { const zend_uchar *p = (zend_uchar *)*packet; + MYSQLND_OPTIONAL_UINT64_T result; + result.has_value = false; + result.value = 0; + + if (UNEXPECTED(remaining_size == 0)) { + return result; + } + if (*p < 251) { (*packet)++; - return (uint64_t) *p; + result.value = (uint64_t) *p; + result.has_value = true; + return result; } switch (*p) { case 251: (*packet)++; - return (uint64_t) MYSQLND_NULL_LENGTH; + result.value = MYSQLND_NULL_LENGTH; + result.has_value = true; + return result; case 252: - (*packet) += 3; - return (uint64_t) uint2korr(p + 1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + result.value = (uint64_t) uint2korr(p+1); + result.has_value = true; + } + return result; case 253: - (*packet) += 4; - return (uint64_t) uint3korr(p + 1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + result.value = (uint64_t) uint3korr(p+1); + result.has_value = true; + } + return result; default: - (*packet) += 9; - return (uint64_t) uint8korr(p + 1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + result.value = uint8korr(p+1); + result.has_value = true; + } + return result; } } /* }}} */ @@ -353,14 +397,14 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "greeting", PROT_GREET_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->authentication_plugin_data.s = packet->intern_auth_plugin_data; packet->authentication_plugin_data.l = sizeof(packet->intern_auth_plugin_data); packet->protocol_version = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->protocol_version) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -380,43 +424,42 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) /* MariaDB always sends 5.5.5 before version string: 5.5.5 was never released, so just ignore it */ - if (!strncmp((char *) p, MARIADB_RPL_VERSION_HACK, sizeof(MARIADB_RPL_VERSION_HACK) - 1)) { + if (packet->header.size - (p - begin) >= sizeof(MARIADB_RPL_VERSION_HACK) - 1 && !strncmp((char *) p, MARIADB_RPL_VERSION_HACK, sizeof(MARIADB_RPL_VERSION_HACK) - 1)) { p += sizeof(MARIADB_RPL_VERSION_HACK) - 1; } - packet->server_version = estrdup((char *)p); - p+= strlen(packet->server_version) + 1; /* eat the '\0' */ - BAIL_IF_NO_MORE_DATA; + /* This server version string MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *version_nul_byte = memchr(p, '\0', packet->header.size - (p - begin)); + if (!version_nul_byte) { + BAIL_PREMATURE_END; + } + packet->server_version = estrndup((char *) p, version_nul_byte - (const char *) p); + p = (unsigned char *) version_nul_byte + 1; /* eat the '\0' */ + + BAIL_IF_NOT_ENOUGH_DATA_EX(4 + SCRAMBLE_LENGTH_323 + 1 + 2 + 1 + 2 + 13); packet->thread_id = uint4korr(p); p+=4; - BAIL_IF_NO_MORE_DATA; memcpy(packet->authentication_plugin_data.s, p, SCRAMBLE_LENGTH_323); p+= SCRAMBLE_LENGTH_323; - BAIL_IF_NO_MORE_DATA; /* pad1 */ p++; - BAIL_IF_NO_MORE_DATA; packet->server_capabilities = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; DBG_INF_FMT("4.1 server_caps=%u\n", (uint32_t) packet->server_capabilities); packet->charset_no = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* pad2 */ pad_start = p; p+= 13; - BAIL_IF_NO_MORE_DATA; if ((size_t) (p - buf) < packet->header.size) { /* auth_plugin_data is split into two parts */ @@ -438,6 +481,9 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) /* And a length of the server scramble in one byte */ packet->authentication_plugin_data.l = uint1korr(pad_start + 2); if (packet->authentication_plugin_data.l > SCRAMBLE_LENGTH) { + /* the rest of the scramble has to be in the packet */ + BAIL_IF_NOT_ENOUGH_DATA_EX(packet->authentication_plugin_data.l - SCRAMBLE_LENGTH); + /* more data*/ char * new_auth_plugin_data = emalloc(packet->authentication_plugin_data.l); @@ -451,11 +497,14 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) } if (packet->server_capabilities & CLIENT_PLUGIN_AUTH) { - BAIL_IF_NO_MORE_DATA; + /* This is actually checked above and it needs to pass to set extended server capabilities + * so it should never bail. */ + BAIL_IF_NOT_ENOUGH_DATA; /* The server is 5.5.x and supports authentication plugins */ size_t remaining_size = packet->header.size - (size_t)(p - buf); if (remaining_size == 0) { - /* Might be better to fail but this will fail anyway */ + /* This should never happen as the size should be at least 1 but it is kept just in + * case something above changes. */ packet->auth_protocol = estrdup(""); } else { /* Check if NUL present */ @@ -489,9 +538,8 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("GREET packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "GREET packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("GREET packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "GREET packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -685,12 +733,12 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always 0x0 or ERROR_MARKER for error */ packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -702,9 +750,14 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -715,27 +768,37 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_INF_FMT("Server salt : [%zu][%.*s]", packet->new_auth_protocol_data_len, (int) packet->new_auth_protocol_data_len, packet->new_auth_protocol_data); } } else { - zend_ulong net_len; /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong net_len; + MYSQLND_OPTIONAL_UINT64_T len_ll; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the net_len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + /* In older versions of mysqlnd we had to perform an extra check because reading the field length + * could overread the OK packet. Now reading the length won't go out of bounds and we only have + * to check whether the message length is in bounds. */ + ZEND_ASSERT((p - buf) <= packet->header.size); + if (packet->header.size - (p - buf) < net_len) { DBG_ERR_FMT("OK packet message length is past the packet size"); php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size"); DBG_RETURN(FAIL); @@ -754,9 +817,8 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -845,12 +907,12 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always 0x0 or ERROR_MARKER for error */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -860,22 +922,33 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + MYSQLND_OPTIONAL_UINT64_T len_ll; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + if (UNEXPECTED(net_len == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } packet->message_len = MIN(net_len, buf_len - (p - begin)); packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE); } else { @@ -887,13 +960,10 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) packet->affected_rows, packet->last_insert_id, packet->server_status, packet->warning_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "OK packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "OK packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -938,12 +1008,12 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "EOF", PROT_EOF_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always EODATA_MARKER */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -959,28 +1029,25 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * conn, void * _packet) according to the Docs@Forge!!! */ if (packet->header.size > 1) { + BAIL_IF_NOT_ENOUGH_DATA_EX(4); + packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; } else { packet->warning_count = 0; packet->server_status = 0; } - BAIL_IF_NO_MORE_DATA; - DBG_INF_FMT("EOF packet: fields=%u status=%u warnings=%u", packet->field_count, packet->server_status, packet->warning_count); DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("EOF packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "EOF packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("EOF packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "EOF packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1061,13 +1128,14 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) const zend_uchar * p = buf; const zend_uchar * const begin = buf; size_t len; + MYSQLND_OPTIONAL_UINT64_T len_ll; DBG_ENTER("php_mysqlnd_rset_header_read"); if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "resultset header", PROT_RSET_HEADER_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -1076,7 +1144,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1084,8 +1152,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } - packet->field_count = php_mysqlnd_net_field_length(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong field_count = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(field_count == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } + packet->field_count = field_count; switch (packet->field_count) { case MYSQLND_NULL_LENGTH: @@ -1093,7 +1164,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) /* First byte in the packet is the field count. Thus, the name is size - 1. And we add 1 for a trailing \0. - Because we have BAIL_IF_NO_MORE_DATA before the switch, we are guaranteed + Because we have BAIL_IF_NOT_ENOUGH_DATA before the switch, we are guaranteed that packet->header.size is > 0. Which means that len can't underflow, that would lead to 0 byte allocation but 2^32 or 2^64 bytes copied. */ @@ -1105,24 +1176,34 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) break; case 0x00: DBG_INF("UPSERT"); - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; + + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + /* Check for additional textual data */ - if (packet->header.size > (size_t) (p - buf) && (len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < len) { + if (len && len != MYSQLND_INVALID_NET_FIELD_LENGTH) { + /* This checks both whether reading the len was successful + * and that the len is not greater than the packet size */ + if (packet->header.size - (p - buf) < len) { size_t local_file_name_over_read = ((p - buf) - packet->header.size) + len; DBG_ERR_FMT("RSET_HEADER packet additional data length is past %zu bytes the packet size", local_file_name_over_read); @@ -1145,13 +1226,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) /* Result set */ break; } - BAIL_IF_NO_MORE_DATA; DBG_RETURN(ret); premature_end: - DBG_ERR_FMT("RSET_HEADER packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "RSET_HEADER packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("RSET_HEADER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "RSET_HEADER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1169,8 +1248,8 @@ void php_mysqlnd_rset_header_free_mem(void * _packet) /* }}} */ #define READ_RSET_FIELD(field_name) do { \ - len = php_mysqlnd_net_field_length(&p); \ - if (UNEXPECTED(len == MYSQLND_NULL_LENGTH)) { \ + zend_ulong len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); \ + if (UNEXPECTED(len > packet->header.size - (p - begin))) { \ goto faulty_or_fake; \ } else if (len != 0) { \ meta->field_name = (const char *)p; \ @@ -1214,11 +1293,11 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1282,9 +1361,8 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) } /* COM_FIELD_LIST is no longer supported so def should not be present */ - if (packet->header.size > (size_t) (p - buf) && - (len = php_mysqlnd_net_field_length(&p)) && - len != MYSQLND_NULL_LENGTH) + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (len < MYSQLND_INVALID_NET_FIELD_LENGTH) { DBG_ERR_FMT("Protocol error. Server sent default for unsupported field list"); php_error_docref(NULL, E_WARNING, @@ -1352,9 +1430,8 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) " The server is faulty"); DBG_RETURN(FAIL); premature_end: - DBG_ERR_FMT("RSET field packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "Result set field packet %zu bytes " - "shorter than expected", p - begin - packet->header.size); + DBG_ERR_FMT("RSET field packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "Result set field packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1580,12 +1657,12 @@ php_mysqlnd_rowp_read_text_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fiel for (i = 0, current_field = start_field; current_field < end_field; current_field++, i++) { /* php_mysqlnd_net_field_length() call should be after *this_field_len_pos = p; */ - const zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p); + zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p, packet_end - p); /* NULL or NOT NULL, this is the question! */ if (len == MYSQLND_NULL_LENGTH) { ZVAL_NULL(current_field); - } else if (p > packet_end || len > packet_end - p) { + } else if (len > packet_end - p) { php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after end of packet"); for (j = 0, current_field = start_field; j < i; current_field++, j++) { zval_ptr_dtor(current_field); @@ -1759,7 +1836,8 @@ php_mysqlnd_rowp_read(MYSQLND_CONN_DATA * conn, void * _packet) } else if (EODATA_MARKER == *p && data_size < 8) { /* EOF */ packet->eof = TRUE; p++; - if (data_size > 1) { + /* the marker is followed by 2 bytes of warnings and 2 bytes of status */ + if (data_size >= 5) { packet->warning_count = uint2korr(p); p += 2; packet->server_status = uint2korr(p); @@ -1844,12 +1922,12 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "prepare", PROT_PREPARE_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; data_size = packet->header.size; packet->error_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->error_code) { php_mysqlnd_read_error_from_line(p, data_size - 1, @@ -1869,23 +1947,23 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(FAIL); } + BAIL_IF_NOT_ENOUGH_DATA_EX(8); + packet->stmt_id = uint4korr(p); p += 4; - BAIL_IF_NO_MORE_DATA; /* Number of columns in result set */ packet->field_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; packet->param_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; if (data_size > 9) { + BAIL_IF_NOT_ENOUGH_DATA_EX(3); + /* 0x0 filler sent by the server for 5.0+ clients */ p++; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); } @@ -1893,13 +1971,10 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_INF_FMT("Prepare packet read: stmt_id=" ZEND_ULONG_FMT " fields=%u params=%u", packet->stmt_id, packet->field_count, packet->param_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("PREPARE packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "PREPARE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("PREPARE packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "PREPARE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1926,7 +2001,7 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "change user response", PROT_CHG_USER_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -1951,11 +2026,16 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) packet->error_info.sqlstate ); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (packet->response_code == 0xFE && packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { packet->new_auth_protocol_data = mnd_emalloc(packet->new_auth_protocol_data_len); @@ -1967,9 +2047,8 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("CHANGE_USER packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "CHANGE_USER packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("CHANGE_USER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "CHANGE_USER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2038,10 +2117,10 @@ php_mysqlnd_sha256_pk_request_response_read(MYSQLND_CONN_DATA * conn, void * _pa if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "SHA256_PK_REQUEST_RESPONSE", PROT_SHA256_PK_REQUEST_RESPONSE_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->public_key_len = packet->header.size - (p - buf); packet->public_key = mnd_emalloc(packet->public_key_len + 1); @@ -2051,9 +2130,8 @@ php_mysqlnd_sha256_pk_request_response_read(MYSQLND_CONN_DATA * conn, void * _pa DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2118,11 +2196,11 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "PROT_CACHED_SHA2_RESULT_PACKET", PROT_CACHED_SHA2_RESULT_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -2134,9 +2212,14 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -2153,20 +2236,27 @@ php_mysqlnd_cached_sha2_result_read(MYSQLND_CONN_DATA * conn, void * _packet) DBG_ERR_FMT("Unexpected response code %d", packet->response_code); } - /* This is not really the response code, but we reuse the field. */ + BAIL_IF_NOT_ENOUGH_DATA; + + /* This is not really the response code, but we reuse the field. This should be either 0x03 if + * fast auth is used or 0x04 if full auth should be done. */ packet->response_code = uint1korr(p); - p++; - BAIL_IF_NO_MORE_DATA; - packet->result = uint1korr(p); - BAIL_IF_NO_MORE_DATA; + /* This should be removed in master and it is really kept for just in case scenario that should + * never happen because mysql-server sends only byte that is stored above. The result seems not + * to be used so there is not much point to set it in any case. */ + if (UNEXPECTED((size_t)(p - begin) + 2 <= packet->header.size)) { + p++; + packet->result = uint1korr(p); + } else { + packet->result = 0; + } DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %zu bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet %zu bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.h b/ext/mysqlnd/mysqlnd_wireprotocol.h index e5d38bc6737e..ad452ec4f928 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.h +++ b/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -293,9 +293,19 @@ typedef struct st_mysqlnd_packet_cached_sha2_result { unsigned int error_no; } MYSQLND_PACKET_CACHED_SHA2_RESULT; +/* The following structure implements an optional value, + * which forces the caller to check if the value was correctly read, for safety reasons. */ +typedef struct st_mysqlnd_optional_uint64_t { + uint64_t value; + bool has_value; +} MYSQLND_OPTIONAL_UINT64_T; -zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet); +/* must not be equal to MYSQLND_NULL_LENGTH, but larger than 2**24 */ +#define MYSQLND_INVALID_NET_FIELD_LENGTH ((zend_ulong) -2) + +/* Returns MYSQLND_INVALID_NET_FIELD_LENGTH on error */ +zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size); zend_uchar * php_mysqlnd_net_store_length(zend_uchar *packet, const uint64_t length); size_t php_mysqlnd_net_store_length_size(uint64_t length); diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index e91da6aeb8d3..85109c7d0e03 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3100,23 +3100,10 @@ static int zend_real_jit_func(zend_op_array *op_array, zend_script *script, cons return FAILURE; } -/* Run-time JIT handler */ -#if ZEND_VM_KIND == ZEND_VM_KIND_CALL || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) -#else -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) -#endif +/* GCC cannot tail-call from a function that uses setjmp. */ +static zend_never_inline void zend_runtime_jit_compile(zend_op_array *op_array) { -#if GCC_GLOBAL_REGS - zend_execute_data *execute_data; - zend_op *opline; -#else - const zend_op *orig_opline = opline; -#endif - - execute_data = EG(current_execute_data); - zend_op_array *op_array = &EX(func)->op_array; - opline = op_array->opcodes; + const zend_op *opline = op_array->opcodes; zend_jit_op_array_extension *jit_extension; bool do_bailout = 0; @@ -3154,6 +3141,23 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(Z if (do_bailout) { zend_bailout(); } +} + +/* Run-time JIT handler */ +#if ZEND_VM_KIND == ZEND_VM_KIND_CALL || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) +#else +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_runtime_jit(ZEND_OPCODE_HANDLER_ARGS) +#endif +{ +#if GCC_GLOBAL_REGS + zend_execute_data *execute_data; +#else + const zend_op *orig_opline = opline; +#endif + + execute_data = EG(current_execute_data); + zend_runtime_jit_compile(&EX(func)->op_array); /* JIT-ed code is going to be called by VM */ #if GCC_GLOBAL_REGS diff --git a/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt b/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt new file mode 100644 index 000000000000..daf5b878bbf8 --- /dev/null +++ b/ext/openssl/tests/GHSA-xr7j-rvgx-xq5p.phpt @@ -0,0 +1,67 @@ +--TEST-- +GHSA-xr7j-rvgx-xq5p: OOB read in php_openssl_matches_wildcard_name() with overlapping wildcard prefix and suffix +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'aaaa'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'www.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('a*aaaa', $certFile, null, $san); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `aaaa' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) + +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `www.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_absent_cn_fallback.phpt b/ext/openssl/tests/san_absent_cn_fallback.phpt new file mode 100644 index 000000000000..30f4821b5fe5 --- /dev/null +++ b/ext/openssl/tests/san_absent_cn_fallback.phpt @@ -0,0 +1,61 @@ +--TEST-- +Peer verification falls back to CN when the certificate has no SAN +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'other.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('cn.example.org', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +resource(%d) of type (stream) + +Warning: stream_socket_client(): Peer certificate CN=`cn.example.org' did not match expected CN=`other.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_no_cn_fallback.phpt b/ext/openssl/tests/san_no_cn_fallback.phpt new file mode 100644 index 000000000000..ae19be414ee7 --- /dev/null +++ b/ext/openssl/tests/san_no_cn_fallback.phpt @@ -0,0 +1,64 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has a SAN +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The SAN entry is matched as usual. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'san.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); + + // The CN must not be considered because the certificate presents a SAN. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'DNS:san.example.org'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +resource(%d) of type (stream) + +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_peer_matching.phpt b/ext/openssl/tests/san_peer_matching.phpt index 0df18f4a9250..cca4d360d8bd 100644 --- a/ext/openssl/tests/san_peer_matching.phpt +++ b/ext/openssl/tests/san_peer_matching.phpt @@ -54,7 +54,7 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); --EXPECTF-- resource(%d) of type (stream) -Warning: stream_socket_client(): Unable to locate peer certificate CN in %s on line %d +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `moar.example.org' in %s on line %d Warning: stream_socket_client(): Failed to enable crypto in %s on line %d diff --git a/ext/openssl/tests/san_srv_no_cn_fallback.phpt b/ext/openssl/tests/san_srv_no_cn_fallback.phpt new file mode 100644 index 000000000000..9c130e757b78 --- /dev/null +++ b/ext/openssl/tests/san_srv_no_cn_fallback.phpt @@ -0,0 +1,57 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has an SRV-ID +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The CN must not be considered because the certificate presents an SRV-ID. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'otherName:1.3.6.1.5.5.7.8.7;IA5STRING:_https.san.example.org'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/tests/san_uri_no_cn_fallback.phpt b/ext/openssl/tests/san_uri_no_cn_fallback.phpt new file mode 100644 index 000000000000..b621d52312bc --- /dev/null +++ b/ext/openssl/tests/san_uri_no_cn_fallback.phpt @@ -0,0 +1,57 @@ +--TEST-- +Peer verification does not fall back to CN when the certificate has a URI-ID +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); + phpt_notify_server_start($server); + + @stream_socket_accept($server, 1); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $serverUri = "ssl://{{ ADDR }}"; + $clientFlags = STREAM_CLIENT_CONNECT; + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + ]]); + + // The CN must not be considered because the certificate presents a URI-ID. + stream_context_set_option($clientCtx, 'ssl', 'peer_name', 'cn.example.org'); + var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx)); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey( + 'cn.example.org', $certFile, null, 'URI:https://san.example.org/x'); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Warning: stream_socket_client(): Peer certificate subjectAltName did not match expected name `cn.example.org' in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to ssl://127.0.0.1:%d (Unknown error) in %s on line %d +bool(false) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 269de9545388..24895a960043 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -475,8 +475,7 @@ static bool php_openssl_x509_fingerprint_match(php_stream *stream, X509 *peer, c static bool php_openssl_matches_wildcard_name(const char *subjectname, const char *certname) /* {{{ */ { const char *wildcard = NULL; - ptrdiff_t prefix_len; - size_t suffix_len, subject_len; + size_t prefix_len, suffix_len, subject_len; if (strcasecmp(subjectname, certname) == 0) { return true; @@ -495,7 +494,7 @@ static bool php_openssl_matches_wildcard_name(const char *subjectname, const cha suffix_len = strlen(wildcard + 1); subject_len = strlen(subjectname); - if (suffix_len <= subject_len) { + if (suffix_len + prefix_len <= subject_len) { /* 2) suffix must match * 3) no . between prefix and suffix **/ @@ -507,7 +506,8 @@ static bool php_openssl_matches_wildcard_name(const char *subjectname, const cha } /* }}} */ -static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) /* {{{ */ +static bool php_openssl_matches_san_list( + X509 *peer, const char *subject_name, bool *has_service_id) { int i, len; unsigned char *cert_name = NULL; @@ -516,6 +516,8 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / GENERAL_NAMES *alt_names = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0); int alt_name_count = sk_GENERAL_NAME_num(alt_names); + *has_service_id = false; + #ifdef HAVE_IPV6_SAN /* detect if subject name is an IPv6 address and expand once if required */ char subject_name_ipv6_expanded[40]; @@ -533,6 +535,8 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / GENERAL_NAME *san = sk_GENERAL_NAME_value(alt_names, i); if (san->type == GEN_DNS) { + *has_service_id = true; + if (ASN1_STRING_to_UTF8(&cert_name, san->d.dNSName) < 0) { /* TODO: warn ? */ continue; @@ -581,6 +585,16 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / } } #endif + } else if (san->type == GEN_URI) { + *has_service_id = true; + } else if (san->type == GEN_OTHERNAME) { + char oid[32]; + + /* SRV-ID, matched by OID because NID_SRVName needs OpenSSL 3.0 */ + if (OBJ_obj2txt(oid, sizeof(oid), san->d.otherName->type_id, 1) > 0 + && strcmp(oid, "1.3.6.1.5.5.7.8.7") == 0) { + *has_service_id = true; + } } } @@ -588,7 +602,6 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) / return false; } -/* }}} */ static bool php_openssl_matches_common_name(php_stream *stream, const X509 *peer, const char *subject_name) /* {{{ */ { @@ -715,8 +728,16 @@ static zend_result php_openssl_apply_peer_verification_policy(SSL *ssl, X509 *pe } if (peer_name) { - if (php_openssl_matches_san_list(peer, peer_name)) { + bool has_service_id = false; + + if (php_openssl_matches_san_list(peer, peer_name, &has_service_id)) { return SUCCESS; + } else if (has_service_id) { + /* CN must be ignored if the certificate presents a service identity. */ + php_stream_warn(stream, AuthFailed, + "Peer certificate subjectAltName did not match expected name `%s'", + peer_name); + return FAILURE; } else if (php_openssl_matches_common_name(stream, peer, peer_name)) { return SUCCESS; } else { diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index 30d863b02f09..7047d10a6c7d 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -78,8 +78,13 @@ #define TAR_FILE '0' #define TAR_LINK '1' #define TAR_SYMLINK '2' +#define TAR_CHAR '3' +#define TAR_BLOCK '4' #define TAR_DIR '5' +#define TAR_FIFO '6' #define TAR_NEW '8' +#define TAR_LONGLINK 'K' +#define TAR_LONGNAME 'L' #define TAR_GLOBAL_HDR 'g' #define TAR_FILE_HDR 'x' diff --git a/ext/phar/tar.c b/ext/phar/tar.c index bc78472afce3..d1858511ee68 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -36,6 +36,52 @@ static uint32_t phar_tar_number(const char *buf, size_t len) /* {{{ */ } /* }}} */ +static bool phar_tar_type_has_data(char typeflag) +{ + switch (typeflag) { + case TAR_LINK: + case TAR_SYMLINK: + case TAR_CHAR: + case TAR_BLOCK: + case TAR_DIR: + case TAR_FIFO: + return false; + default: + return true; + } +} + +static bool phar_tar_size(const char *buf, size_t len, uint32_t *result) +{ + uint64_t num = 0; + size_t i = 0; + + while (i < len && buf[i] == ' ') { + ++i; + } + + /* GNU base-256 encoding is only used for sizes that do not fit the octal field */ + if (i < len && (((unsigned char) buf[i]) & 0x80)) { + return false; + } + + while (i < len && buf[i] >= '0' && buf[i] <= '7') { + num = num * 8 + (buf[i] - '0'); + ++i; + } + + while (i < len && (buf[i] == ' ' || buf[i] == '\0')) { + ++i; + } + + if (i != len || num > UINT32_MAX - 511) { + return false; + } + + *result = (uint32_t) num; + return true; +} + /* adapted from format_octal() in libarchive * * Copyright (c) 2003-2009 Tim Kientzle @@ -278,8 +324,32 @@ zend_result phar_parse_tarfile( } } - size = entry.uncompressed_filesize = entry.compressed_filesize = - phar_tar_number(hdr->size, sizeof(hdr->size)); + if (!phar_tar_size(hdr->size, sizeof(hdr->size), &size)) { + if (error) { + spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); + } + if (last_was_longlink) { + zend_string_free(entry.filename); + } + php_stream_close(fp); + phar_destroy_phar_data(myphar); + return FAILURE; + } + entry.uncompressed_filesize = entry.compressed_filesize = size; + + /* GNU long link names are not supported, so refuse the record instead of + * registering it as an entry and dropping the link target of the entry that follows */ + if (hdr->typeflag == TAR_LONGLINK) { + if (error) { + spprintf(error, 4096, "phar error: \"%s\" is a tar file with an unsupported GNU long link entry", fname); + } + if (last_was_longlink) { + zend_string_free(entry.filename); + } + php_stream_close(fp); + phar_destroy_phar_data(myphar); + return FAILURE; + } /* skip global/file headers (pax) */ if (!old && (hdr->typeflag == TAR_GLOBAL_HDR || hdr->typeflag == TAR_FILE_HDR)) { @@ -367,12 +437,12 @@ zend_result phar_parse_tarfile( goto bail; } - if (!last_was_longlink && hdr->typeflag == 'L') { + if (!last_was_longlink && hdr->typeflag == TAR_LONGNAME) { last_was_longlink = true; /* support the ././@LongLink system for storing long filenames */ /* Check for overflow - bug 61065 */ - if (entry.uncompressed_filesize == UINT_MAX || entry.uncompressed_filesize == 0) { + if (entry.uncompressed_filesize == 0 || entry.uncompressed_filesize > totalsize) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); } @@ -591,7 +661,7 @@ zend_result phar_parse_tarfile( size = (size+511)&~511; - if (((hdr->typeflag == '\0') || (hdr->typeflag == TAR_FILE)) && size > 0) { + if (phar_tar_type_has_data(hdr->typeflag) && size > 0) { next: /* this is not good enough - seek succeeds even on truncated tars */ php_stream_seek(fp, size, SEEK_CUR); diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt new file mode 100644 index 000000000000..5d05ac311ef0 --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-longlink.phpt @@ -0,0 +1,50 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (././@LongLink name longer than the archive) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +$fname = __DIR__ . '/' . basename(__FILE__, '.php') . '.tar'; + +/* 2 GB of file name announced by a 2 KB archive */ +$tar = tar_header('././@LongLink', "20000000000\0", 'L'); +$tar .= str_pad('long.txt', 512, "\0"); +$tar .= tar_header('short.txt', sprintf("%011o\0", 3)) . str_pad('abc', 512, "\0"); +$tar .= str_repeat("\0", 1024); +file_put_contents($fname, $tar); + +try { + new PharData($fname); +} catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECTF-- +phar error: "%s" is a corrupted tar file (invalid entry size) diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt new file mode 100644 index 000000000000..731422a0250b --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-size.phpt @@ -0,0 +1,92 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (tar entry injection via invalid entry size) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +function tar_dump(string $fname): void +{ + try { + $phar = new PharData($fname); + $names = []; + foreach (new RecursiveIteratorIterator($phar) as $file) { + $names[] = $file->getFilename(); + } + sort($names); + echo implode(', ', $names), "\n"; + } catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; + } +} + +$base = __DIR__ . '/' . basename(__FILE__, '.php'); + +/* the entry after the carrier is never reached by a conforming tar reader */ +$sizes = [ + 'octal overflow to 0' => "40000000000\0", + 'octal overflow to 1' => "40000000001\0", + 'octal padding overflow' => "37777777400\0", + 'GNU base-256' => "\x80" . str_repeat("\0", 9) . "\x02\x00", + 'non-octal digits' => "99999999999\0", + 'trailing garbage' => "0000000001XX", +]; + +$i = 0; +foreach ($sizes as $label => $size) { + $fname = $base . '.' . $i++ . '.tar'; + $tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); + $tar .= tar_header('carrier.bin', $size); + $tar .= tar_header('injected.txt', sprintf("%011o\0", 8)) . str_pad('INJECTED', 512, "\0"); + $tar .= str_repeat("\0", 1024); + file_put_contents($fname, $tar); + + echo $label, ': '; + tar_dump($fname); +} + +$fname = $base . '.valid.tar'; +$tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); +$tar .= tar_header('second.txt', sprintf("%011o\0", 8)) . str_pad('SECOND__', 512, "\0"); +$tar .= str_repeat("\0", 1024); +file_put_contents($fname, $tar); + +echo 'valid archive: '; +tar_dump($fname); +?> +--CLEAN-- + +--EXPECTF-- +octal overflow to 0: phar error: "%s" is a corrupted tar file (invalid entry size) +octal overflow to 1: phar error: "%s" is a corrupted tar file (invalid entry size) +octal padding overflow: phar error: "%s" is a corrupted tar file (invalid entry size) +GNU base-256: phar error: "%s" is a corrupted tar file (invalid entry size) +non-octal digits: phar error: "%s" is a corrupted tar file (invalid entry size) +trailing garbage: phar error: "%s" is a corrupted tar file (invalid entry size) +valid archive: normal.txt, second.txt diff --git a/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt new file mode 100644 index 000000000000..227d33a4823b --- /dev/null +++ b/ext/phar/tests/tar/GHSA-j3wh-g957-2m85-typeflag.phpt @@ -0,0 +1,79 @@ +--TEST-- +GHSA-j3wh-g957-2m85 (tar entry injection via entry types that carry data) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- += 148 && $i < 156) ? 0x20 : ord($header[$i]); + } + + return substr_replace($header, sprintf("%06o\0 ", $checksum), 148, 8); +} + +$base = __DIR__ . '/' . basename(__FILE__, '.php'); + +/* '5' and the other types below carry no data in any tar reader, so the + * injected header stays visible to all of them and is not hidden from + * anything that inspects the archive with a different implementation. + * 'K' is a GNU metadata record that is not supported and is refused. */ +$typeflags = [ + 'contiguous file' => '7', + 'unknown type' => 'Z', + 'GNU long link' => 'K', + 'directory' => '5', + 'fifo' => '6', +]; + +$i = 0; +foreach ($typeflags as $label => $typeflag) { + $fname = $base . '.' . $i++ . '.tar'; + $tar = tar_header('normal.txt', sprintf("%011o\0", 9)) . str_pad('NORMAL_OK', 512, "\0"); + $tar .= tar_header('carrier.bin', sprintf("%011o\0", 512), $typeflag); + /* the carrier's only data block is a tar header a conforming reader skips */ + $tar .= tar_header('injected.txt', sprintf("%011o\0", 0)); + $tar .= str_repeat("\0", 1024); + file_put_contents($fname, $tar); + + echo $label, ': '; + try { + $phar = new PharData($fname); + $names = []; + foreach (new RecursiveIteratorIterator($phar) as $file) { + $names[] = $file->getFilename(); + } + sort($names); + echo implode(', ', $names), "\n"; + } catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--CLEAN-- + +--EXPECTF-- +contiguous file: carrier.bin, normal.txt +unknown type: carrier.bin, normal.txt +GNU long link: phar error: "%s" is a tar file with an unsupported GNU long link entry +directory: injected.txt, normal.txt +fifo: carrier.bin, injected.txt, normal.txt diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c index 044d2058ec09..646dc688abfb 100644 --- a/ext/soap/php_encoding.c +++ b/ext/soap/php_encoding.c @@ -543,7 +543,12 @@ static zval *master_to_zval_int(zval *ret, encodePtr encode, xmlNodePtr data) } } if (encode->to_zval) { + if (SOAP_GLOBAL(decode_depth) >= SOAP_MAX_DECODE_DEPTH) { + soap_error0(E_ERROR, "Encoding: Nesting level too deep"); + } + SOAP_GLOBAL(decode_depth)++; ret = encode->to_zval(ret, &encode->details, data); + SOAP_GLOBAL(decode_depth)--; } return ret; } @@ -3546,6 +3551,7 @@ void encode_reset_ns(void) { SOAP_GLOBAL(cur_uniq_ns) = 0; SOAP_GLOBAL(cur_uniq_ref) = 0; + SOAP_GLOBAL(decode_depth) = 0; if (SOAP_GLOBAL(ref_map)) { zend_hash_destroy(SOAP_GLOBAL(ref_map)); } else { @@ -3558,6 +3564,7 @@ void encode_finish(void) { SOAP_GLOBAL(cur_uniq_ns) = 0; SOAP_GLOBAL(cur_uniq_ref) = 0; + SOAP_GLOBAL(decode_depth) = 0; if (SOAP_GLOBAL(ref_map)) { zend_hash_destroy(SOAP_GLOBAL(ref_map)); efree(SOAP_GLOBAL(ref_map)); diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 5df9506102af..e7325ec65b53 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -1461,7 +1461,8 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h zend_string *http_buf = NULL; char *header; bool header_close = close, header_chunked = false; - int header_length = 0, http_buf_size = 0; + int header_length = 0; + size_t http_buf_size = 0; if (!close) { header = get_http_header_value(headers, "Connection:"); @@ -1494,14 +1495,14 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h bool done = false; while (!done) { - int buf_size = 0; + unsigned int buf_size = 0; php_stream_gets(stream, headerbuf, sizeof(headerbuf)); if (sscanf(headerbuf, "%x", &buf_size) > 0 ) { if (buf_size > 0) { size_t len_size = 0; - if (UNEXPECTED(http_buf_size + buf_size + 1 < 0)) { + if (UNEXPECTED(buf_size >= ZSTR_MAX_LEN - http_buf_size)) { if (http_buf) { zend_string_release_ex(http_buf, 0); } @@ -1509,7 +1510,7 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h } if (http_buf) { - http_buf = zend_string_realloc(http_buf, http_buf_size + buf_size, 0); + http_buf = zend_string_safe_realloc(http_buf, 1, http_buf_size, buf_size, false); } else { http_buf = zend_string_alloc(buf_size, 0); } @@ -1568,7 +1569,7 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h } } else if (header_length) { - if (UNEXPECTED(header_length < 0 || header_length >= INT_MAX)) { + if (UNEXPECTED(header_length < 0 || header_length >= ZSTR_MAX_LEN)) { return NULL; } http_buf = zend_string_alloc(header_length, 0); @@ -1583,7 +1584,11 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h do { ssize_t len_read; if (http_buf) { - http_buf = zend_string_realloc(http_buf, http_buf_size + 4096, 0); + if (UNEXPECTED(http_buf_size >= ZSTR_MAX_LEN - 4096)) { + zend_string_efree(http_buf); + return NULL; + } + http_buf = zend_string_realloc(http_buf, http_buf_size + 4096, false); } else { http_buf = zend_string_alloc(4096, 0); } diff --git a/ext/soap/php_soap.h b/ext/soap/php_soap.h index 8d127ef1e5d5..d3c273615eb6 100644 --- a/ext/soap/php_soap.h +++ b/ext/soap/php_soap.h @@ -148,6 +148,8 @@ struct _soapService { #define SOAP_SSL_METHOD_SSLv3 2 #define SOAP_SSL_METHOD_SSLv23 3 +#define SOAP_MAX_XML_DEPTH 2048 +#define SOAP_MAX_DECODE_DEPTH (SOAP_MAX_XML_DEPTH * 2) ZEND_BEGIN_MODULE_GLOBALS(soap) HashTable *typemap; @@ -169,6 +171,7 @@ ZEND_BEGIN_MODULE_GLOBALS(soap) HashTable wsdl_cache; int cur_uniq_ref; HashTable *ref_map; + unsigned int decode_depth; ZEND_END_MODULE_GLOBALS(soap) extern zend_string *soap_lang_en; diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c index a4c638410a73..72e562c1fd41 100644 --- a/ext/soap/php_xml.c +++ b/ext/soap/php_xml.c @@ -33,36 +33,72 @@ static bool is_blank(const xmlChar* str) return true; } -/* removes all empty text, comments and other insignificant nodes */ +/* removes all empty text, comments and other insignificant nodes. + * Iterative because recursion overflows the stack on a deep document. */ static void cleanup_xml_node(xmlNodePtr node) { - xmlNodePtr trav; - xmlNodePtr del = NULL; + xmlNodePtr parent = node; + xmlNodePtr trav = node->children; - trav = node->children; while (trav != NULL) { - if (del != NULL) { - xmlUnlinkNode(del); - xmlFreeNode(del); - del = NULL; - } + xmlNodePtr next = trav->next; + if (trav->type == XML_TEXT_NODE) { if (is_blank(trav->content)) { - del = trav; + xmlUnlinkNode(trav); + xmlFreeNode(trav); } } else if ((trav->type != XML_ELEMENT_NODE) && (trav->type != XML_CDATA_SECTION_NODE)) { - del = trav; + xmlUnlinkNode(trav); + xmlFreeNode(trav); } else if (trav->children != NULL) { - cleanup_xml_node(trav); + parent = trav; + trav = trav->children; + continue; } - trav = trav->next; + + while (next == NULL) { + if (parent == node) { + return; + } + next = parent->next; + parent = parent->parent; + } + trav = next; } - if (del != NULL) { - xmlUnlinkNode(del); - xmlFreeNode(del); +} + +#if LIBXML_VERSION < 21300 +static int is_nesting_too_deep(xmlNodePtr node) +{ + xmlNodePtr trav = node->children; + unsigned int depth = 0; + + while (trav != NULL) { + /* An entity reference borrows its declaration as child list, and that + * declaration hangs off the DTD, so descending leaves the document. */ + if (trav->children != NULL && + trav->type != XML_ENTITY_REF_NODE && + trav->type != XML_DTD_NODE) { + if (++depth > SOAP_MAX_XML_DEPTH) { + return TRUE; + } + trav = trav->children; + continue; + } + while (trav->next == NULL) { + trav = trav->parent; + if (trav == node) { + return FALSE; + } + depth--; + } + trav = trav->next; } + return FALSE; } +#endif static void soap_ignorableWhitespace(void *ctx, const xmlChar *ch, int len) { @@ -121,6 +157,15 @@ xmlDocPtr soap_xmlParseFile(const char *filename) xmlDocPtr ret = soap_xmlParse_ex(ctxt); if (ret) { +#if LIBXML_VERSION < 21300 + if (is_nesting_too_deep((xmlNodePtr)ret)) { + /* php_sdl.c reports xmlGetLastError() as the reason, and libxml2 did + * not fail here, so drop the error an earlier parse left behind. */ + xmlResetLastError(); + xmlFreeDoc(ret); + return NULL; + } +#endif cleanup_xml_node((xmlNodePtr)ret); } return ret; @@ -131,6 +176,13 @@ xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size) xmlParserCtxtPtr ctxt = xmlCreateMemoryParserCtxt(buf, buf_size); xmlDocPtr ret = soap_xmlParse_ex(ctxt); +#if LIBXML_VERSION < 21300 + if (ret && is_nesting_too_deep((xmlNodePtr)ret)) { + xmlFreeDoc(ret); + ret = NULL; + } +#endif + /* if (ret) { cleanup_xml_node((xmlNodePtr)ret); @@ -259,6 +311,8 @@ xmlNodePtr get_node_with_attribute_ex(xmlNodePtr node, const char *name, const c xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, const char *name, const char *name_ns, const char *attribute, const char *value, const char *attr_ns) { + unsigned int depth = 0; + while (node != NULL) { if (node_is_equal_ex(node, name, name_ns)) { xmlAttrPtr attr = get_attribute_ex(node->properties, attribute, attr_ns); @@ -266,11 +320,19 @@ xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, const char *nam return node; } } - if (node->children != NULL) { - xmlNodePtr tmp = get_node_with_attribute_recursive_ex(node->children, name, name_ns, attribute, value, attr_ns); - if (tmp) { - return tmp; + if (node->children != NULL && + node->type != XML_ENTITY_REF_NODE && + node->type != XML_DTD_NODE) { + node = node->children; + depth++; + continue; + } + while (node->next == NULL) { + if (depth == 0) { + return NULL; } + node = node->parent; + depth--; } node = node->next; } diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 752e1c6361e1..97a653955825 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -471,6 +471,7 @@ static void php_soap_init_globals(zend_soap_globals *soap_globals) soap_globals->soap_version = SOAP_1_1; soap_globals->mem_cache = NULL; soap_globals->ref_map = NULL; + soap_globals->decode_depth = 0; } PHP_MSHUTDOWN_FUNCTION(soap) diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt new file mode 100644 index 000000000000..e07f78d816f3 --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-chain.phpt @@ -0,0 +1,47 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on a chain of href references +--EXTENSIONS-- +soap +--FILE-- +' . ($i < $n ? '' : 'leaf') . ''; + } + + return '

' + . $links . ''; +} + +function test($arg) +{ + $GLOBALS['decoded'] = $arg; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(chain(3)); +var_dump($GLOBALS['decoded']); + +$server->handle(chain(12000)); + +?> +--EXPECTF-- + +ok +object(stdClass)#%d (1) { + ["q"]=> + object(stdClass)#%d (1) { + ["q"]=> + string(4) "leaf" + } +} + +SOAP-ENV:ServerSOAP-ERROR: Encoding: Nesting level too deep diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt new file mode 100644 index 000000000000..5058ba109bf8 --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm-href-cycle.phpt @@ -0,0 +1,43 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on an array element referencing its own array +--EXTENSIONS-- +soap +--FILE-- +' + . '' . $param . ''; +} + +function test($arg) +{ + $GLOBALS['decoded'] = $arg; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(envelope('ab')); +var_dump($GLOBALS['decoded']); + +$server->handle(envelope('')); + +?> +--EXPECTF-- + +ok +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} + +SOAP-ENV:ServerSOAP-ERROR: Encoding: Nesting level too deep diff --git a/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt new file mode 100644 index 000000000000..f3213962b47f --- /dev/null +++ b/ext/soap/tests/GHSA-rgrp-mwpx-f6rm.phpt @@ -0,0 +1,89 @@ +--TEST-- +GHSA-rgrp-mwpx-f6rm: Stack overflow on deeply nested XML +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--FILE-- +' + . str_repeat('', $depth) . 'leaf' . str_repeat('', $depth) + . ''; +} + +function test($arg) +{ + $depth = 0; + while (is_object($arg)) { + $depth++; + $arg = $arg->a; + } + $GLOBALS['decoded'] = [$depth, $arg]; + + return 'ok'; +} + +$server = new SoapServer(null, ['uri' => 'urn:test']); +$server->addFunction('test'); + +$server->handle(envelope(200)); +var_dump($GLOBALS['decoded']); + +/* libxml2 2.13 rejects past 2048 despite XML_PARSE_HUGE, older ones do not. */ +$wsdl = __DIR__ . '/GHSA-rgrp-mwpx-f6rm.wsdl'; +foreach ([1000, 2100] as $depth) { + file_put_contents($wsdl, '' . str_repeat('', $depth) . 'leaf' . str_repeat('', $depth) . ''); + try { + new SoapClient($wsdl); + } catch (SoapFault $e) { + echo rtrim($e->getMessage()), "\n"; + } +} + +/* A WSDL reached through an entity reference still loads. Its replacement tree cannot + be built deeper than the 2048 libxml2 2.13 enforces, so this pins the entity skip + rather than the limit. */ +$entity = __DIR__ . '/GHSA-rgrp-mwpx-f6rm-entity.wsdl'; +$desc = str_repeat('<p>', 300) . 'A test service.' . str_repeat('</p>', 300); +file_put_contents($entity, ' +]> + + &desc; + + + + + + + +'); +var_dump((new SoapClient($entity))->__getFunctions()); + +$server->handle(envelope(3000)); + +?> +--CLEAN-- + +--EXPECTF-- + +ok +array(2) { + [0]=> + int(199) + [1]=> + string(4) "leaf" +} +SOAP-ERROR: Parsing WSDL: Couldn't find in '%sGHSA-rgrp-mwpx-f6rm.wsdl' +SOAP-ERROR: Parsing WSDL: Couldn't load from '%sGHSA-rgrp-mwpx-f6rm.wsdl'%S +array(1) { + [0]=> + string(9) "void op()" +} + +SOAP-ENV:ClientBad Request diff --git a/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt new file mode 100644 index 000000000000..c96b67bb77fa --- /dev/null +++ b/ext/soap/tests/bugs/GHSA-cj93-vc83-wgqv.phpt @@ -0,0 +1,156 @@ +--TEST-- +GHSA-cj93-vc83-wgqv +--INI-- +soap.wsdl_cache_enabled=0 +memory_limit=-1 +--EXTENSIONS-- +soap +--CONFLICTS-- +all +--SKIPIF-- + 6) { + $enough_free_ram = true; + } + } + } +} + +if (empty($enough_free_ram)) { + die(sprintf("skip need +6G free RAM, but only %01.2f available", $free_ram)); +} +--FILE-- + $v) { + $chunks[$k] = sprintf("%08x\r\n%s\r\n", strlen($v), $v); + } + + return join('', $chunks); +} + +$wsdl = file_get_contents(__DIR__.'/../server030.wsdl'); + +$headers = + "HTTP/1.1 200 OK\r\n". + "Content-Type: text/xml;charset=utf-8\r\n". + "Transfer-Encoding: \t chunked\t \r\n". + "Connection: close\r\n". + "\r\n"; + +/* Custom minimal server. Unlike the generic http_server() helper it streams the + * 2 GiB filler in bounded blocks instead of materialising it (and a data:// + * copy of it) in memory. That keeps the sender's footprint tiny: only the + * client needs to hold the large buffers, so total memory and run time stay far + * lower and the test no longer thrashes on slower machines. */ +function heavy_soap_server($wsdl, $headers) +{ + $server = stream_socket_server('tcp://localhost:0', $errno, $errstr); + if (!$server) { + return false; + } + $uri = 'http://' . stream_socket_get_name($server, false); + + $pid = pcntl_fork(); + if ($pid == -1) { + die('could not fork'); + } else if ($pid) { + return ['pid' => $pid, 'uri' => $uri]; + } + + /* Child: streaming 2 GiB can exceed the 60s alarm the helper would use, so + * match the run-tests per-test timeout instead. */ + pcntl_alarm(120); + + $drain = static function ($sock) { + stream_set_blocking($sock, false); + while (!feof($sock)) { + $r = [$sock]; $w = $e = null; + if (!stream_select($r, $w, $e, 1)) continue; + $line = stream_get_line($sock, 8192, "\r\n"); + if ($line === '') break; + } + stream_set_blocking($sock, true); + }; + + /* Response 1: the WSDL, chunked. */ + $sock = stream_socket_accept($server, 60); + if ($sock) { + $drain($sock); + fwrite($sock, $headers . chunk_body($wsdl, 64)); + fclose($sock); + } + + /* Response 2: an oversized chunk. Only the size header of the second chunk + * is needed: the reallocation for it happens before its body is read, so on + * the unfixed code the overflow triggers on the first read into the + * undersized buffer. The 2 GiB first-chunk body is streamed in 8 MiB blocks + * rather than built as one string. */ + $sock = stream_socket_accept($server, 60); + if ($sock) { + $drain($sock); + fwrite($sock, $headers); + fwrite($sock, sprintf("%08x\r\n", 0x7fffffff)); + + $remaining = 0x7fffffff; + $block = str_repeat('x', 1 << 23); // 8 MiB + $block_len = strlen($block); + while ($remaining > 0) { + $n = $remaining < $block_len ? $remaining : $block_len; + fwrite($sock, $n === $block_len ? $block : substr($block, 0, $n)); + $remaining -= $n; + } + + fwrite($sock, "\r\n"); + fwrite($sock, sprintf("%08x\r\n", 0x7fffffff)); + fwrite($sock, "xxxx"); + fclose($sock); + } + + exit(0); +} + +['pid' => $pid, 'uri' => $uri] = heavy_soap_server($wsdl, $headers); + +$options = [ + 'trace' => false, + 'location' => $uri, +]; + +$client = new SoapClient($uri, $options); + +$client->getItems(); + +http_server_kill($pid); + +--EXPECTF-- +Fatal error: Uncaught SoapFault exception: [HTTP] Error Fetching http body, No Content-Length, connection closed or chunked data in %s:%d +Stack trace: +#0 [internal function]: SoapClient->__doRequest('__call('getItems', Array) +#2 {main} + thrown in %s on line %d diff --git a/ext/sockets/tests/bug63000.phpt b/ext/sockets/tests/bug63000.phpt index 04265638a260..99723d68cdca 100644 --- a/ext/sockets/tests/bug63000.phpt +++ b/ext/sockets/tests/bug63000.phpt @@ -2,6 +2,23 @@ Bug #63000: Multicast on OSX --EXTENSIONS-- sockets +--SKIPIF-- + '224.0.0.251', + 'interface' => 0, +]); +if ($so === false) { + $errno = socket_last_error($socket); + if (in_array($errno, [SOCKET_ENODEV, SOCKET_ENXIO, SOCKET_EADDRNOTAVAIL], true)) { + die('skip no multicast-capable interface: ' . socket_strerror($errno)); + } +} +?> --FILE-- line_ccnt = line_len; inst->line_len = line_len; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; @@ -875,7 +875,7 @@ static php_conv_err_t php_conv_qprint_encode_ctor(php_conv_qprint_encode *inst, inst->line_ccnt = line_len; inst->line_len = line_len; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; @@ -1104,7 +1104,7 @@ static php_conv_err_t php_conv_qprint_decode_ctor(php_conv_qprint_decode *inst, inst->next_char = 0; inst->lb_ptr = inst->lb_cnt = 0; if (lbchars != NULL) { - inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars); + inst->lbchars = (lbchars_dup ? pestrndup(lbchars, lbchars_len, persistent) : lbchars); inst->lbchars_len = lbchars_len; } else { inst->lbchars = NULL; diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 4a0f95062bcd..cfe4e8e7a95b 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -73,27 +73,70 @@ #define HTTP_WRAPPER_HEADER_INIT 1 #define HTTP_WRAPPER_REDIRECTED 2 #define HTTP_WRAPPER_KEEP_METHOD 4 +#define HTTP_WRAPPER_STRIP_AUTH 8 +static char *next_header_line(char *line) +{ + while (*line != '\0' && *line != '\r' && *line != '\n') { + line++; + } + if (*line == '\r') { + line++; + } + if (*line == '\n') { + line++; + } + + return line; +} + +/* Removes every line whose header name matches, along with the folded + * continuation lines carrying the rest of its value. Neither a repeated header + * nor an occurrence of the name inside another header's value may leave the real + * header behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */ static inline void strip_header(char *header_bag, char *lc_header_bag, const char *lc_header_name) { - char *lc_header_start = strstr(lc_header_bag, lc_header_name); - if (lc_header_start - && (lc_header_start == lc_header_bag || *(lc_header_start-1) == '\n') - ) { - char *header_start = header_bag + (lc_header_start - lc_header_bag); - char *lc_eol = strchr(lc_header_start, '\n'); + size_t name_len = strlen(lc_header_name); + char *lc_line = lc_header_bag; - if (lc_eol) { - char *eol = header_start + (lc_eol - lc_header_start); - size_t eollen = strlen(lc_eol); + while (*lc_line != '\0') { + if (strncmp(lc_line, lc_header_name, name_len) != 0) { + lc_line = next_header_line(lc_line); + continue; + } - memmove(lc_header_start, lc_eol+1, eollen); - memmove(header_start, eol+1, eollen); - } else { - *lc_header_start = '\0'; - *header_start = '\0'; + /* the whitespace RFC 7230 forbids before the colon is tolerated by some + * servers, so it must not hide the header from us either */ + const char *lc_colon = lc_line + name_len; + while (*lc_colon == ' ' || *lc_colon == '\t') { + lc_colon++; + } + + if (*lc_colon != ':') { + lc_line = next_header_line(lc_line); + continue; } + + char *lc_next = next_header_line(lc_line); + while (*lc_next == ' ' || *lc_next == '\t') { + lc_next = next_header_line(lc_next); + } + + if (*lc_next == '\0') { + /* drop the preceding line break too, or the one appended after the bag + * would close the header block early */ + while (lc_line > lc_header_bag + && (*(lc_line - 1) == '\r' || *(lc_line - 1) == '\n')) { + --lc_line; + } + } + + size_t tail_len = strlen(lc_next) + 1; + char *line = header_bag + (lc_line - lc_header_bag); + + memmove(line, header_bag + (lc_next - lc_header_bag), tail_len); + memmove(lc_line, lc_next, tail_len); } } @@ -710,8 +753,23 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, if (!header_init && !redirect_keep_method) { /* strip POST headers on redirect */ - strip_header(user_headers, t, "content-length:"); - strip_header(user_headers, t, "content-type:"); + strip_header(user_headers, t, "content-length"); + strip_header(user_headers, t, "content-type"); + } + + if (flags & HTTP_WRAPPER_STRIP_AUTH) { + strip_header(user_headers, t, "authorization"); + strip_header(user_headers, t, "cookie"); + if (!use_proxy) { + strip_header(user_headers, t, "proxy-authorization"); + } + } + + if (*user_headers == '\0') { + /* everything got stripped, keeping the empty bag would append a + * stray CRLF and end the header block early */ + efree(user_headers); + user_headers = NULL; } if (check_has_header(t, "user-agent:")) { @@ -1099,15 +1157,26 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, header_info.location = NULL; } - php_uri_struct_free(resource); /* check for invalid redirection URLs */ - if ((resource = php_uri_parse_to_struct(uri_parser, new_path, strlen(new_path), PHP_URI_COMPONENT_READ_MODE_RAW, true)) == NULL) { + php_uri *new_resource = php_uri_parse_to_struct(uri_parser, new_path, strlen(new_path), PHP_URI_COMPONENT_READ_MODE_RAW, true); + if (new_resource == NULL) { php_stream_wrapper_log_warn(wrapper, context, options, InvalidUrl, "Invalid redirect URL! %s", new_path); efree(new_path); goto out; } + zend_long default_port = use_ssl ? 443 : 80; + bool same_origin = resource->scheme && new_resource->scheme + && zend_string_equals_ci(resource->scheme, new_resource->scheme) + && resource->host && new_resource->host + && zend_string_equals_ci(resource->host, new_resource->host) + && (resource->port ? resource->port : default_port) + == (new_resource->port ? new_resource->port : default_port); + + php_uri_struct_free(resource); + resource = new_resource; + #define CHECK_FOR_CNTRL_CHARS(val) { \ if (val) { \ unsigned char *s, *e; \ @@ -1130,7 +1199,10 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, CHECK_FOR_CNTRL_CHARS(resource->password); CHECK_FOR_CNTRL_CHARS(resource->path); } - int new_flags = HTTP_WRAPPER_REDIRECTED; + int new_flags = HTTP_WRAPPER_REDIRECTED | (flags & HTTP_WRAPPER_STRIP_AUTH); + if (!same_origin) { + new_flags |= HTTP_WRAPPER_STRIP_AUTH; + } if (response_code == 307 || response_code == 308) { /* RFC 7538 specifies that status code 308 does not allow changing the request method from POST to GET. * RFC 7231 does the same for status code 307. diff --git a/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt b/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt new file mode 100644 index 000000000000..d83260e5507e --- /dev/null +++ b/ext/standard/tests/file/ghsa-9f67-6fw4-hpfp-win32.phpt @@ -0,0 +1,86 @@ +--TEST-- +GHSA-9f67-6fw4-hpfp: Windows reserved device names are rejected in filesystem paths +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Bare device names keep working: +bool(true) +bool(true) +bool(true) +Reserved names in paths are rejected: +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Similar names are fine: +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt b/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt new file mode 100644 index 000000000000..84a85daefdb9 --- /dev/null +++ b/ext/standard/tests/filters/ghsa-88hq-2827-7pg6.phpt @@ -0,0 +1,27 @@ +--TEST-- +GHSA-88hq-2827-7pg6: OOB read in convert stream filters when line-break-chars contains NUL +--FILE-- + 4, "line-break-chars" => "\0X"]; +$fp = fopen("php://temp", "r+"); +fwrite($fp, str_repeat("A", 6)); +rewind($fp); +stream_filter_append($fp, "convert.base64-encode", STREAM_FILTER_READ, $opts); +$out = stream_get_contents($fp); +fclose($fp); + +// base64("AAAAAA") = "QUFBQUFB" (8 chars) +// With line-length=4: "QUFB" + "\0X" + "QUFB" +// Hex: 5155464200585155 4642 +echo bin2hex($out), "\n"; + +echo "Done\n"; + +?> +--EXPECT-- +51554642005851554642 +Done diff --git a/ext/standard/tests/http/bug61548.phpt b/ext/standard/tests/http/bug61548.phpt index 5f21b3769dd8..ba46e65704f3 100644 --- a/ext/standard/tests/http/bug61548.phpt +++ b/ext/standard/tests/http/bug61548.phpt @@ -55,7 +55,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close @@ -69,7 +68,6 @@ Connection: close First:1 Second:2 - POST / HTTP/1.1 Host: %s:%d Connection: close diff --git a/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt new file mode 100644 index 000000000000..c314a24ea2eb --- /dev/null +++ b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92-002.phpt @@ -0,0 +1,61 @@ +--TEST-- +GHSA-fpwc-w8rq-cr92: stripping the last user header must not cut the redirected request short +--INI-- +allow_url_fopen=1 +--SKIPIF-- + +--FILE-- + [ + 'method' => 'POST', + 'header' => "X-Test: 1\r\nContent-Type: text/plain\r\nAuthorization: Basic Zm9vOmJhcg==", + 'content' => 'hello=world', + 'follow_location' => 1, +]]); + +$captureB = null; +['pid' => $pidB, 'uri' => $uriB] = http_server([ + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureB); + +$captureA = null; +['pid' => $pidA, 'uri' => $uriA] = http_server([ + "data://text/plain,HTTP/1.1 307 Temporary Redirect\r\nLocation: $uriB/target\r\nContent-Length: 0\r\n\r\n", +], $captureA); + +var_dump(file_get_contents($uriA . '/start', false, $ctx)); + +http_server_kill($pidA); +http_server_kill($pidB); + +rewind($captureA); +echo "--- origin A ---\n", stream_get_contents($captureA), "\n"; +rewind($captureB); +echo "--- origin B ---\n", stream_get_contents($captureB), "\n"; +?> +--EXPECTF-- +string(2) "OK" +--- origin A --- +POST /start HTTP/1.1 +Host: %s:%d +Connection: close +Content-Length: 11 +X-Test: 1 +Content-Type: text/plain +Authorization: Basic Zm9vOmJhcg== + +hello=world +--- origin B --- +POST /target HTTP/1.1 +Host: %s:%d +Connection: close +Content-Length: 11 +X-Test: 1 +Content-Type: text/plain + +hello=world diff --git a/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt new file mode 100644 index 000000000000..21a8b911a34b --- /dev/null +++ b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt @@ -0,0 +1,85 @@ +--TEST-- +GHSA-fpwc-w8rq-cr92: strip credentials from user headers on cross-origin redirect +--INI-- +allow_url_fopen=1 +--SKIPIF-- + +--FILE-- + [ + 'header' => "Authorization: Bearer SECRET\r\n" + . "Cookie: sid=abc\r\n" + . "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n" + . "X-Custom: keep-me", + 'follow_location' => 1, +]]); + +/* server B listens on a different port than server A, so the hop from A to B is + * cross-origin; B then redirects to itself: credentials must stay withheld for + * that same-origin hop too */ +$captureB = null; +['pid' => $pidB, 'uri' => $uriB] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /second\r\nContent-Length: 0\r\n\r\n", + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureB); + +$captureA = null; +['pid' => $pidA, 'uri' => $uriA] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriB/first\r\nContent-Length: 0\r\n\r\n", +], $captureA); + +var_dump(file_get_contents($uriA . '/src', false, $ctx)); + +http_server_kill($pidA); +http_server_kill($pidB); + +rewind($captureA); +rewind($captureB); +report('--- origin A (1 request) ---', stream_get_contents($captureA)); +report('--- origin B (2 requests) ---', stream_get_contents($captureB)); + +/* same origin throughout: credentials must be sent on both hops */ +$captureC = null; +['pid' => $pidC, 'uri' => $uriC] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n", + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", +], $captureC); + +var_dump(file_get_contents($uriC . '/first', false, $ctx)); + +http_server_kill($pidC); + +rewind($captureC); +report('--- origin C (2 requests) ---', stream_get_contents($captureC)); +?> +--EXPECT-- +string(2) "OK" +--- origin A (1 request) --- + Authorization: 1 + Cookie: 1 + Proxy-Authorization: 1 + X-Custom: 1 +--- origin B (2 requests) --- + Authorization: 0 + Cookie: 0 + Proxy-Authorization: 0 + X-Custom: 2 +string(2) "OK" +--- origin C (2 requests) --- + Authorization: 2 + Cookie: 2 + Proxy-Authorization: 2 + X-Custom: 2 diff --git a/main/fastcgi.c b/main/fastcgi.c index abd558928036..a393283fcb0b 100644 --- a/main/fastcgi.c +++ b/main/fastcgi.c @@ -1320,7 +1320,7 @@ static int fcgi_is_allowed(void) { if (client_sa.sa.sa_family == AF_INET6) { for (i = 0; allowed_clients[i].sa.sa_family ; i++) { if (allowed_clients[i].sa.sa_family == AF_INET6 - && !memcmp(&client_sa.sa_inet6.sin6_addr, &allowed_clients[i].sa_inet6.sin6_addr, 12)) { + && !memcmp(&client_sa.sa_inet6.sin6_addr, &allowed_clients[i].sa_inet6.sin6_addr, sizeof(client_sa.sa_inet6.sin6_addr))) { return 1; } #ifdef IN6_IS_ADDR_V4MAPPED diff --git a/main/php.h b/main/php.h index 275d07309ca1..89ceac346b50 100644 --- a/main/php.h +++ b/main/php.h @@ -20,7 +20,7 @@ #include #endif -#define PHP_API_VERSION 20250926 +#define PHP_API_VERSION 20260925 #define YYDEBUG 0 #define PHP_DEFAULT_CHARSET "UTF-8" diff --git a/main/php_version.h b/main/php_version.h index fa9484cbe150..515f48bb63c1 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -1,8 +1,8 @@ /* automatically generated by configure */ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 -#define PHP_MINOR_VERSION 6 +#define PHP_MINOR_VERSION 7 #define PHP_RELEASE_VERSION 0 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.6.0-dev" -#define PHP_VERSION_ID 80600 +#define PHP_VERSION "8.7.0-dev" +#define PHP_VERSION_ID 80700 diff --git a/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt b/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt new file mode 100644 index 000000000000..c72f9bcdcc5f --- /dev/null +++ b/sapi/fpm/tests/ghsa-62xp-839h-2637-ipv6-allowed-clients.phpt @@ -0,0 +1,52 @@ +--TEST-- +GHSA-62xp-839h-2637: IPv6 allowed client compared on all 128 bits +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('{{ADDR:IPv6[exact]}}', 'exact: ok', 'exact: error'); +$tester->checkRequest('{{ADDR:IPv6[prefix]}}', 'prefix: ok', 'prefix: error'); +$tester->terminate(); +$tester->expectLogWarning( + 'child %d said into stderr: "ERROR: Connection disallowed: IP address \'::1\' has been dropped."', + 'prefix' +); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +exact: ok +prefix: error +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt b/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt new file mode 100644 index 000000000000..1bbb7c16585f --- /dev/null +++ b/sapi/fpm/tests/ghsa-62xp-839h-2637-v4mapped-allowed-clients.phpt @@ -0,0 +1,52 @@ +--TEST-- +GHSA-62xp-839h-2637: IPv4-mapped allowed client compared on all 128 bits +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('{{ADDR:IPv4[exact]}}', 'exact: ok', 'exact: error'); +$tester->checkRequest('{{ADDR:IPv4[prefix]}}', 'prefix: ok', 'prefix: error'); +$tester->terminate(); +$tester->expectLogWarning( + 'child %d said into stderr: "ERROR: Connection disallowed: IP address \'127.0.0.1\' has been dropped."', + 'prefix' +); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +exact: ok +prefix: error +Done +--CLEAN-- + diff --git a/win32/build/confutils.js b/win32/build/confutils.js index 7d9297e8c2d6..7c2cab480bf5 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -93,10 +93,10 @@ if (typeof(CWD) == "undefined") { if (!MODE_PHPIZE) { /* defaults; we pick up the precise versions from configure.ac */ var PHP_VERSION = 8; - var PHP_MINOR_VERSION = 6; + var PHP_MINOR_VERSION = 7; var PHP_RELEASE_VERSION = 0; var PHP_EXTRA_VERSION = ""; - var PHP_VERSION_STRING = "8.6.0"; + var PHP_VERSION_STRING = "8.7.0"; } /* Get version numbers and DEFINE as a string */ diff --git a/win32/ioutil.c b/win32/ioutil.c index 0d77649796a7..8ac4d39d67cf 100644 --- a/win32/ioutil.c +++ b/win32/ioutil.c @@ -65,6 +65,175 @@ #include */ +typedef ULONG (WINAPI *php_win32_ioutil_rtl_is_dos_device_name_u_t)(PCWSTR); + +/* Resolved on first use as there is no ioutil init hook on this branch. */ +static php_win32_ioutil_rtl_is_dos_device_name_u_t php_win32_ioutil_get_rtl_is_dos_device_name_u(void) +{/*{{{*/ + static php_win32_ioutil_rtl_is_dos_device_name_u_t fn = NULL; + static BOOL resolved = FALSE; + + if (!resolved) { + HMODULE hMod = GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + fn = (php_win32_ioutil_rtl_is_dos_device_name_u_t)GetProcAddress(hMod, "RtlIsDosDeviceName_U"); + } + resolved = TRUE; + } + + return fn; +}/*}}}*/ + +static BOOL php_win32_ioutil_is_reserved_name_w(const wchar_t *name, size_t len) +{/*{{{*/ + if (len == 3) { + return _wcsnicmp(name, L"CON", 3) == 0 + || _wcsnicmp(name, L"PRN", 3) == 0 + || _wcsnicmp(name, L"AUX", 3) == 0 + || _wcsnicmp(name, L"NUL", 3) == 0; + } + + if (len == 4 && (_wcsnicmp(name, L"COM", 3) == 0 || _wcsnicmp(name, L"LPT", 3) == 0)) { + return (name[3] >= L'1' && name[3] <= L'9') + || name[3] == L'\u00B2' + || name[3] == L'\u00B3' + || name[3] == L'\u00B9'; + } + + return (len == 6 && _wcsnicmp(name, L"CONIN$", 6) == 0) + || (len == 7 && _wcsnicmp(name, L"CONOUT$", 7) == 0); +}/*}}}*/ + +/* Also catches variants like NUL.txt, NUL:stream or "NUL ", as far as the OS treats them as devices. */ +static BOOL php_win32_ioutil_is_reserved_component_w(const wchar_t *name, size_t len) +{/*{{{*/ + size_t base_len = len; + wchar_t *tmp; + BOOL ret; + ALLOCA_FLAG(use_heap) + + if (php_win32_ioutil_is_reserved_name_w(name, len)) { + return TRUE; + } + + for (size_t i = 0; i < len; i++) { + if (name[i] == L'.' || name[i] == L':') { + base_len = i; + break; + } + } + while (base_len > 0 && name[base_len - 1] == L' ') { + base_len--; + } + + if (base_len == len || !php_win32_ioutil_is_reserved_name_w(name, base_len)) { + return FALSE; + } + + php_win32_ioutil_rtl_is_dos_device_name_u_t rtl_is_dos_device_name_u = php_win32_ioutil_get_rtl_is_dos_device_name_u(); + if (!rtl_is_dos_device_name_u) { + return TRUE; + } + + tmp = do_alloca((len + 1) * sizeof(wchar_t), use_heap); + memcpy(tmp, name, len * sizeof(wchar_t)); + tmp[len] = L'\0'; + ret = rtl_is_dos_device_name_u(tmp) > 0; + free_alloca(tmp, use_heap); + + return ret; +}/*}}}*/ + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_w(const wchar_t *path, size_t path_len) +{/*{{{*/ + size_t i = 0; + + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + + if (i == path_len && !(path_len > 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':')) { + if (path_len == 0) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + /* Bare device names like NUL or NUL: are kept working for BC. */ + if (php_win32_ioutil_is_reserved_name_w(path, path_len - (path[path_len - 1] == L':'))) { + return PHP_WIN32_IOUTIL_PATH_DEVICE; + } + return php_win32_ioutil_is_reserved_component_w(path, path_len) + ? PHP_WIN32_IOUTIL_PATH_RESERVED : PHP_WIN32_IOUTIL_PATH_OK; + } + + /* Windows does not map device names within DOS device paths (\\.\, \\?\ and \??\). */ + if (path_len >= 4 && PHP_WIN32_IOUTIL_IS_SLASHW(path[3]) + && ((PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1]) && (path[2] == L'.' || path[2] == L'?')) + || (path[0] == L'\\' && path[1] == L'?' && path[2] == L'?'))) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + + i = 0; + if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1])) { + /* UNC, skip server and share. */ + for (int n = 0; n < 2; n++) { + while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + } + } else if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':') { + i = 2; + } + + while (i < path_len) { + size_t start; + + while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + start = i; + while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) { + i++; + } + if (i > start && php_win32_ioutil_is_reserved_component_w(path + start, i - start)) { + return PHP_WIN32_IOUTIL_PATH_RESERVED; + } + } + + return PHP_WIN32_IOUTIL_PATH_OK; +}/*}}}*/ + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_a(const char *path, size_t path_len) +{/*{{{*/ + wchar_t *pathw; + size_t i, pathw_len; + php_win32_ioutil_path_kind ret; + ALLOCA_FLAG(use_heap) + + for (i = 0; i < path_len && !(path[i] & 0x80); i++); + + if (i < path_len) { + pathw = php_win32_cp_conv_any_to_w(path, path_len, &pathw_len); + if (!pathw) { + return PHP_WIN32_IOUTIL_PATH_OK; + } + ret = php_win32_ioutil_path_kind_w(pathw, pathw_len); + free(pathw); + return ret; + } + + /* ASCII only, widen on the stack instead of a full conversion. */ + pathw = do_alloca((path_len + 1) * sizeof(wchar_t), use_heap); + for (i = 0; i < path_len; i++) { + pathw[i] = (wchar_t) path[i]; + } + ret = php_win32_ioutil_path_kind_w(pathw, path_len); + free_alloca(pathw, use_heap); + + return ret; +}/*}}}*/ + PW32IO BOOL php_win32_ioutil_posix_to_open_opts(int flags, mode_t mode, php_ioutil_open_opts *opts) {/*{{{*/ int current_umask; @@ -663,6 +832,8 @@ PW32IO int php_win32_ioutil_access_w(const wchar_t *path, mode_t mode) {/*{{{*/ DWORD attr; + PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0) + if ((mode & X_OK) == X_OK) { DWORD type; return GetBinaryTypeW(path, &type) ? 0 : -1; @@ -972,6 +1143,8 @@ PW32IO int php_win32_ioutil_stat_ex_w(const wchar_t *path, size_t path_len, php_ int ret; ALLOCA_FLAG(use_heap_large) + PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0) + hLink = CreateFileW(path, FILE_READ_ATTRIBUTES, PHP_WIN32_IOUTIL_DEFAULT_SHARE_MODE, diff --git a/win32/ioutil.h b/win32/ioutil.h index 7ed222d61382..81dd3e6f44ef 100644 --- a/win32/ioutil.h +++ b/win32/ioutil.h @@ -108,6 +108,15 @@ typedef enum { PHP_WIN32_IOUTIL_NORM_FAIL, } php_win32_ioutil_normalization_result; +typedef enum { + PHP_WIN32_IOUTIL_PATH_OK, + PHP_WIN32_IOUTIL_PATH_DEVICE, + PHP_WIN32_IOUTIL_PATH_RESERVED, +} php_win32_ioutil_path_kind; + +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_w(const wchar_t *path, size_t path_len); +PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_a(const char *path, size_t path_len); + #define PHP_WIN32_IOUTIL_FW_SLASHW L'/' #define PHP_WIN32_IOUTIL_FW_SLASH '/' #define PHP_WIN32_IOUTIL_BW_SLASHW L'\\' @@ -153,7 +162,8 @@ typedef enum { #define PHP_WIN32_IOUTIL_PATH_IS_OK_W(pathw, len) \ (!((len) >= 1 && L' ' == pathw[(len)-1] || \ - (len) > 1 && !PHP_WIN32_IOUTIL_IS_SLASHW(pathw[(len)-2]) && L'.' != pathw[(len)-2] && L'.' == pathw[(len)-1])) + (len) > 1 && !PHP_WIN32_IOUTIL_IS_SLASHW(pathw[(len)-2]) && L'.' != pathw[(len)-2] && L'.' == pathw[(len)-1]) \ + && PHP_WIN32_IOUTIL_PATH_RESERVED != php_win32_ioutil_path_kind_w(pathw, len)) #define PHP_WIN32_IOUTIL_CHECK_PATH_W(pathw, ret, dealloc) do { \ size_t _len = wcslen(pathw); \