diff options
| author | Remi Collet <remi@remirepo.net> | 2026-05-13 16:31:07 +0200 |
|---|---|---|
| committer | Remi Collet <remi@php.net> | 2026-05-13 16:31:07 +0200 |
| commit | 11b950e90bcd9f0f3a7906cd3f2ae0c2e323f860 (patch) | |
| tree | 625fdbf1da40e924f0daaf8e6a065643c9c08058 | |
| parent | daf9b88c6cd0fe21b83f684e10dba7095d49605e (diff) | |
CVE-2026-6735
Fix Stale SOAP_GLOBAL(ref_map) pointer with Apache Map
CVE-2026-6722
Fix Use-after-free after header parsing failure with SOAP_PERSISTENCE_SESSION
CVE-2026-7261
Fix Broken Apache map value NULL check
CVE-2026-7262
Fix Signed integer overflow of char array offset
CVE-2026-7568
| -rw-r--r-- | failed.txt | 2 | ||||
| -rw-r--r-- | php-cve-2026-6722.patch | 113 | ||||
| -rw-r--r-- | php-cve-2026-6735.patch | 2697 | ||||
| -rw-r--r-- | php-cve-2026-7261.patch | 122 | ||||
| -rw-r--r-- | php-cve-2026-7262.patch | 83 | ||||
| -rw-r--r-- | php-cve-2026-7568.patch | 90 | ||||
| -rw-r--r-- | php-fpm.service | 2 | ||||
| -rw-r--r-- | php.spec | 29 |
8 files changed, 3133 insertions, 5 deletions
@@ -1,4 +1,4 @@ -===== 7.0.33-45 (2024-11-26) +===== 7.0.33-46 (2026-05-13) $ grep -r 'Tests failed' /var/lib/mock/scl70*/build.log diff --git a/php-cve-2026-6722.patch b/php-cve-2026-6722.patch new file mode 100644 index 0000000..735ee1d --- /dev/null +++ b/php-cve-2026-6722.patch @@ -0,0 +1,113 @@ +From 237b2b14cad370a25ddec00be93a9710003b5048 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sun, 3 May 2026 19:56:53 +0200 +Subject: [PATCH 1/6] GHSA-85c2-q967-79q5: [soap] Fix stale + SOAP_GLOBAL(ref_map) pointer with Apache Map + +Fixes GHSA-85c2-q967-79q5 +Fixes CVE-2026-6722 + +(cherry picked from commit aee3b3ac9b816b0def1c462695b483b49a83148e) +(cherry picked from commit 15064460d6682766f91c1a841d27cdfbc38907e8) +(cherry picked from commit bbc1be3fc763b81707ccaa91a4cd1d439b753b12) +(cherry picked from commit 6c4b67ca091afea4f436202d7f9db38a129106dc) +(cherry picked from commit 017843d76d595ae97cb97eba4affd69501244571) +(cherry picked from commit 8fc3ed35cf67234da5201f64051e2ffa96d70f86) +(cherry picked from commit 7151aacadf978a14d06e09dd5899e8727f232056) +--- + ext/soap/php_encoding.c | 3 +- + ext/soap/tests/GHSA-85c2-q967-79q5.phpt | 61 +++++++++++++++++++++++++ + 2 files changed, 63 insertions(+), 1 deletion(-) + create mode 100644 ext/soap/tests/GHSA-85c2-q967-79q5.phpt + +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index 47afe2703c7..40fba95980a 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -381,6 +381,7 @@ static zend_bool soap_check_xml_ref(zval *data, xmlNodePtr node) + static void soap_add_xml_ref(zval *data, xmlNodePtr node) + { + if (SOAP_GLOBAL(ref_map)) { ++ Z_TRY_ADDREF_P(data); + zend_hash_index_update(SOAP_GLOBAL(ref_map), (zend_ulong)node, data); + } + } +@@ -3472,7 +3473,7 @@ void encode_reset_ns() + } else { + SOAP_GLOBAL(ref_map) = emalloc(sizeof(HashTable)); + } +- zend_hash_init(SOAP_GLOBAL(ref_map), 0, NULL, NULL, 0); ++ zend_hash_init(SOAP_GLOBAL(ref_map), 0, NULL, ZVAL_PTR_DTOR, 0); + } + + void encode_finish() +diff --git a/ext/soap/tests/GHSA-85c2-q967-79q5.phpt b/ext/soap/tests/GHSA-85c2-q967-79q5.phpt +new file mode 100644 +index 00000000000..8bcac26ad18 +--- /dev/null ++++ b/ext/soap/tests/GHSA-85c2-q967-79q5.phpt +@@ -0,0 +1,61 @@ ++--TEST-- ++GHSA-85c2-q967-79q5: Stale SOAP_GLOBAL(ref_map) pointer with Apache Map ++--CREDITS-- ++brettgervasoni ++--EXTENSIONS-- ++soap ++--FILE-- ++<?php ++ ++class Handler { ++ public function test(...$args) { ++ $GLOBALS['result'] = $args; ++ } ++} ++ ++$envelope = <<<'XML' ++<?xml version="1.0" encoding="UTF-8"?> ++<soapenv:Envelope ++ xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" ++ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ++ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> ++ ++ <soapenv:Body> ++ <test> ++ <map xsi:type="apache:Map" xmlns:apache="http://xml.apache.org/xml-soap"> ++ <item> ++ <key>foo</key> ++ <value id="stale"><object>bar</object></value> ++ </item> ++ <item> ++ <key>foo</key> ++ <value>baz</value> ++ </item> ++ </map> ++ <stale href="#stale"/> ++ </test> ++ </soapenv:Body> ++</soapenv:Envelope> ++XML; ++ ++$s = new SoapServer(null, ['uri' => 'urn:a']); ++$s->setClass(Handler::class); ++$s->handle($envelope); ++var_dump($result); ++ ++?> ++--EXPECTF-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:a" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:testResponse><return xsi:nil="true"/></ns1:testResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> ++array(2) { ++ [0]=> ++ array(1) { ++ ["foo"]=> ++ string(3) "baz" ++ } ++ [1]=> ++ object(stdClass)#%d (1) { ++ ["object"]=> ++ string(3) "bar" ++ } ++} +-- +2.54.0 + diff --git a/php-cve-2026-6735.patch b/php-cve-2026-6735.patch new file mode 100644 index 0000000..844bccb --- /dev/null +++ b/php-cve-2026-6735.patch @@ -0,0 +1,2697 @@ +From 382b1902c46ae1965b7973b801d007366a7851c2 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 3 May 2026 20:01:41 +0200 +Subject: [PATCH 5/6] GHSA-7qg2-v9fj-4mwv: [fpm] XSS within status endpoint + +Fixes GHSA-7qg2-v9fj-4mwv +Fixes CVE-2026-6735 + +(cherry picked from commit 99a5ad7441de9914246c7863adb6997396008b9d) +(cherry picked from commit cc2960e782eb5cc262d7bd572a7d18979a811954) +(cherry picked from commit 62daef7b73108ceda2545862cde0673f252ba2d2) +(cherry picked from commit aeaf48ca0bceba42b9595dff30d9e96029c54613) + +backport some new FPM tester features + +(cherry picked from commit 8b1746466f9fcf248f9879fabfa356106d365da0) +(cherry picked from commit 8e0efa0f20484c8bbfdb8671d61b232b70e2bd0a) +(cherry picked from commit 9be5be215954c010087134964d4fd3ab907cdbda) +--- + sapi/fpm/fpm/fpm_status.c | 31 +- + sapi/fpm/tests/fcgi.inc | 192 ++- + .../tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt | 48 + + sapi/fpm/tests/logtool.inc | 476 ++++++ + sapi/fpm/tests/response.inc | 281 ++++ + sapi/fpm/tests/tester.inc | 1279 +++++++++++++++++ + 6 files changed, 2244 insertions(+), 63 deletions(-) + create mode 100644 sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt + create mode 100644 sapi/fpm/tests/logtool.inc + create mode 100644 sapi/fpm/tests/response.inc + create mode 100644 sapi/fpm/tests/tester.inc + +diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c +index 666a1d9aba5..9693f0342ae 100644 +--- a/sapi/fpm/fpm/fpm_status.c ++++ b/sapi/fpm/fpm/fpm_status.c +@@ -386,9 +386,10 @@ int fpm_status_handle_request(void) /* {{{ */ + + /* no need to test the var 'full' */ + if (full_syntax) { +- int i, first; +- zend_string *tmp_query_string; +- char *query_string; ++ unsigned int i; ++ int first; ++ zend_string *tmp_query_string, *tmp_request_uri_string; ++ char *query_string, *request_uri_string; + struct timeval duration, now; + #ifdef HAVE_FPM_LQ + float cpu; +@@ -415,13 +416,30 @@ int fpm_status_handle_request(void) /* {{{ */ + } + } + ++ request_uri_string = NULL; ++ tmp_request_uri_string = NULL; ++ if (proc.request_uri[0] != '\0') { ++ if (encode) { ++ tmp_request_uri_string = php_escape_html_entities_ex( ++ (unsigned char*)proc.request_uri, ++ strlen(proc.request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, ++ NULL, /* double_encode */ 1); ++ request_uri_string = ZSTR_VAL(tmp_request_uri_string); ++ } else { ++ request_uri_string = proc.request_uri; ++ } ++ } ++ + query_string = NULL; + tmp_query_string = NULL; + if (proc.query_string[0] != '\0') { + if (!encode) { + query_string = proc.query_string; + } else { +- tmp_query_string = php_escape_html_entities_ex((unsigned char *)proc.query_string, strlen(proc.query_string), 1, ENT_HTML_IGNORE_ERRORS & ENT_COMPAT, NULL, 1); ++ tmp_query_string = php_escape_html_entities_ex( ++ (unsigned char*)proc.query_string, ++ strlen(proc.query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, ++ NULL, /* double_encode */ 1); + query_string = ZSTR_VAL(tmp_query_string); + } + } +@@ -449,7 +467,7 @@ int fpm_status_handle_request(void) /* {{{ */ + proc.requests, + duration.tv_sec * 1000000UL + duration.tv_usec, + proc.request_method[0] != '\0' ? proc.request_method : "-", +- proc.request_uri[0] != '\0' ? proc.request_uri : "-", ++ request_uri_string ? request_uri_string : "-", + query_string ? "?" : "", + query_string ? query_string : "", + proc.content_length, +@@ -462,6 +480,9 @@ int fpm_status_handle_request(void) /* {{{ */ + PUTS(buffer); + efree(buffer); + ++ if (tmp_request_uri_string) { ++ zend_string_free(tmp_request_uri_string); ++ } + if (tmp_query_string) { + zend_string_free(tmp_query_string); + } +diff --git a/sapi/fpm/tests/fcgi.inc b/sapi/fpm/tests/fcgi.inc +index b31676260da..07603a808e0 100644 +--- a/sapi/fpm/tests/fcgi.inc ++++ b/sapi/fpm/tests/fcgi.inc +@@ -72,25 +72,25 @@ class Client + + /** + * Socket +- * @var Resource ++ * @var resource + */ + private $_sock = null; + + /** + * Host +- * @var String ++ * @var string + */ + private $_host = null; + + /** + * Port +- * @var Integer ++ * @var int + */ + private $_port = null; + + /** + * Keep Alive +- * @var Boolean ++ * @var bool + */ + private $_keepAlive = false; + +@@ -110,27 +110,27 @@ class Client + + /** + * Use persistent sockets to connect to backend +- * @var Boolean ++ * @var bool + */ + private $_persistentSocket = false; + + /** + * Connect timeout in milliseconds +- * @var Integer ++ * @var int + */ + private $_connectTimeout = 5000; + + /** + * Read/Write timeout in milliseconds +- * @var Integer ++ * @var int + */ + private $_readWriteTimeout = 5000; + + /** + * Constructor + * +- * @param String $host Host of the FastCGI application +- * @param Integer $port Port of the FastCGI application ++ * @param string $host Host of the FastCGI application ++ * @param int $port Port of the FastCGI application + */ + public function __construct($host, $port) + { +@@ -138,15 +138,25 @@ class Client + $this->_port = $port; + } + ++ /** ++ * Get host. ++ * ++ * @return string ++ */ ++ public function getHost() ++ { ++ return $this->_host; ++ } ++ + /** + * Define whether or not the FastCGI application should keep the connection + * alive at the end of a request + * +- * @param Boolean $b true if the connection should stay alive, false otherwise ++ * @param bool $b true if the connection should stay alive, false otherwise + */ + public function setKeepAlive($b) + { +- $this->_keepAlive = (boolean)$b; ++ $this->_keepAlive = (bool)$b; + if (!$this->_keepAlive && $this->_sock) { + fclose($this->_sock); + } +@@ -155,7 +165,7 @@ class Client + /** + * Get the keep alive status + * +- * @return Boolean true if the connection should stay alive, false otherwise ++ * @return bool true if the connection should stay alive, false otherwise + */ + public function getKeepAlive() + { +@@ -166,12 +176,12 @@ class Client + * Define whether or not PHP should attempt to re-use sockets opened by previous + * request for efficiency + * +- * @param Boolean $b true if persistent socket should be used, false otherwise ++ * @param bool $b true if persistent socket should be used, false otherwise + */ + public function setPersistentSocket($b) + { + $was_persistent = ($this->_sock && $this->_persistentSocket); +- $this->_persistentSocket = (boolean)$b; ++ $this->_persistentSocket = (bool)$b; + if (!$this->_persistentSocket && $was_persistent) { + fclose($this->_sock); + } +@@ -180,7 +190,7 @@ class Client + /** + * Get the pesistent socket status + * +- * @return Boolean true if the socket should be persistent, false otherwise ++ * @return bool true if the socket should be persistent, false otherwise + */ + public function getPersistentSocket() + { +@@ -191,7 +201,7 @@ class Client + /** + * Set the connect timeout + * +- * @param Integer number of milliseconds before connect will timeout ++ * @param int number of milliseconds before connect will timeout + */ + public function setConnectTimeout($timeoutMs) + { +@@ -201,7 +211,7 @@ class Client + /** + * Get the connect timeout + * +- * @return Integer number of milliseconds before connect will timeout ++ * @return int number of milliseconds before connect will timeout + */ + public function getConnectTimeout() + { +@@ -211,7 +221,7 @@ class Client + /** + * Set the read/write timeout + * +- * @param Integer number of milliseconds before read or write call will timeout ++ * @param int number of milliseconds before read or write call will timeout + */ + public function setReadWriteTimeout($timeoutMs) + { +@@ -222,7 +232,7 @@ class Client + /** + * Get the read timeout + * +- * @return Integer number of milliseconds before read will timeout ++ * @return int number of milliseconds before read will timeout + */ + public function getReadWriteTimeout() + { +@@ -232,14 +242,18 @@ class Client + /** + * Helper to avoid duplicating milliseconds to secs/usecs in a few places + * +- * @param Integer millisecond timeout +- * @return Boolean ++ * @param int millisecond timeout ++ * @return bool + */ + private function set_ms_timeout($timeoutMs) { + if (!$this->_sock) { + return false; + } +- return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000); ++ return stream_set_timeout( ++ $this->_sock, ++ floor($timeoutMs / 1000), ++ ($timeoutMs % 1000) * 1000 ++ ); + } + + +@@ -250,9 +264,21 @@ class Client + { + if (!$this->_sock) { + if ($this->_persistentSocket) { +- $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); ++ $this->_sock = pfsockopen( ++ $this->_host, ++ $this->_port, ++ $errno, ++ $errstr, ++ $this->_connectTimeout/1000 ++ ); + } else { +- $this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); ++ $this->_sock = fsockopen( ++ $this->_host, ++ $this->_port, ++ $errno, ++ $errstr, ++ $this->_connectTimeout/1000 ++ ); + } + + if (!$this->_sock) { +@@ -268,9 +294,10 @@ class Client + /** + * Build a FastCGI packet + * +- * @param Integer $type Type of the packet +- * @param String $content Content of the packet +- * @param Integer $requestId RequestId ++ * @param int $type Type of the packet ++ * @param string $content Content of the packet ++ * @param int $requestId RequestId ++ * @return string + */ + private function buildPacket($type, $content, $requestId = 1) + { +@@ -289,9 +316,9 @@ class Client + /** + * Build an FastCGI Name value pair + * +- * @param String $name Name +- * @param String $value Value +- * @return String FastCGI Name value pair ++ * @param string $name Name ++ * @param string $value Value ++ * @return string FastCGI Name value pair + */ + private function buildNvpair($name, $value) + { +@@ -302,14 +329,16 @@ class Client + $nvpair = chr($nlen); + } else { + /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ +- $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); ++ $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) ++ . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); + } + if ($vlen < 128) { + /* valueLengthB0 */ + $nvpair .= chr($vlen); + } else { + /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ +- $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); ++ $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) ++ . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); + } + /* nameData & valueData */ + return $nvpair . $name . $value; +@@ -318,7 +347,7 @@ class Client + /** + * Read a set of FastCGI Name value pairs + * +- * @param String $data Data containing the set of FastCGI NVPair ++ * @param string $data Data containing the set of FastCGI NVPair + * @return array of NVPair + */ + private function readNvpair($data, $length = null) +@@ -357,7 +386,7 @@ class Client + /** + * Decode a FastCGI Packet + * +- * @param String $data String containing all the packet ++ * @param string $data string containing all the packet + * @return array + */ + private function decodePacketHeader($data) +@@ -403,6 +432,7 @@ class Client + * + * @param array $requestedInfo information to retrieve + * @return array ++ * @throws \Exception + */ + public function getValues(array $requestedInfo) + { +@@ -423,11 +453,14 @@ class Client + } + + /** +- * Execute a request to the FastCGI application ++ * Execute a request to the FastCGI application and return response body + * + * @param array $params Array of parameters +- * @param String $stdin Content +- * @return String ++ * @param string $stdin Content ++ * @return string ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception + */ + public function request(array $params, $stdin) + { +@@ -435,20 +468,38 @@ class Client + return $this->wait_for_response($id); + } + ++ /** ++ * Execute a request to the FastCGI application and return request data ++ * ++ * @param array $params Array of parameters ++ * @param string $stdin Content ++ * @return array ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception ++ */ ++ public function request_data(array $params, $stdin) ++ { ++ $id = $this->async_request($params, $stdin); ++ return $this->wait_for_response_data($id); ++ } ++ + /** + * Execute a request to the FastCGI application asyncronously +- * ++ * + * This sends request to application and returns the assigned ID for that request. + * + * You should keep this id for later use with wait_for_response(). Ids are chosen randomly +- * rather than seqentially to guard against false-positives when using persistent sockets. +- * In that case it is possible that a delayed response to a request made by a previous script +- * invocation comes back on this socket and is mistaken for response to request made with same ID +- * during this request. ++ * rather than sequentially to guard against false-positives when using persistent sockets. ++ * In that case it is possible that a delayed response to a request made by a previous script ++ * invocation comes back on this socket and is mistaken for response to request made with same ++ * ID during this request. + * + * @param array $params Array of parameters +- * @param String $stdin Content +- * @return Integer ++ * @param string $stdin Content ++ * @return int ++ * @throws TimedOutException ++ * @throws \Exception + */ + public function async_request(array $params, $stdin) + { +@@ -460,10 +511,12 @@ class Client + // Using persistent sockets implies you want them keept alive by server! + $keepAlive = intval($this->_keepAlive || $this->_persistentSocket); + +- $request = $this->buildPacket(self::BEGIN_REQUEST +- ,chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5) +- ,$id +- ); ++ $request = $this->buildPacket( ++ self::BEGIN_REQUEST, ++ chr(0) . chr(self::RESPONDER) . chr($keepAlive) ++ . str_repeat(chr(0), 5), ++ $id ++ ); + + $paramsRequest = ''; + foreach ($params as $key => $value) { +@@ -494,21 +547,26 @@ class Client + + $this->_requests[$id] = array( + 'state' => self::REQ_STATE_WRITTEN, +- 'response' => null ++ 'response' => null, ++ 'err_response' => null, ++ 'out_response' => null, + ); + + return $id; + } + + /** +- * Blocking call that waits for response to specific request +- * +- * @param Integer $requestId +- * @param Integer $timeoutMs [optional] the number of milliseconds to wait. Defaults to the ReadWriteTimeout value set. +- * @return string response body ++ * Blocking call that waits for response data of the specific request ++ * ++ * @param int $requestId ++ * @param int $timeoutMs [optional] the number of milliseconds to wait. ++ * @return array response data ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception + */ +- public function wait_for_response($requestId, $timeoutMs = 0) { +- ++ public function wait_for_response_data($requestId, $timeoutMs = 0) ++ { + if (!isset($this->_requests[$requestId])) { + throw new \Exception('Invalid request id given'); + } +@@ -537,12 +595,15 @@ class Client + if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { + if ($resp['type'] == self::STDERR) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR; ++ $this->_requests[$resp['requestId']]['err_response'] .= $resp['content']; ++ } else { ++ $this->_requests[$resp['requestId']]['out_response'] .= $resp['content']; + } + $this->_requests[$resp['requestId']]['response'] .= $resp['content']; + } + if ($resp['type'] == self::END_REQUEST) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK; +- if ($resp['requestId'] == $requestId) { ++ if ($resp['requestId'] == $requestId) { + break; + } + } +@@ -586,7 +647,22 @@ class Client + throw new \Exception('Role value not known [UNKNOWN_ROLE]'); + break; + case self::REQUEST_COMPLETE: +- return $this->_requests[$requestId]['response']; ++ return $this->_requests[$requestId]; + } + } ++ ++ /** ++ * Blocking call that waits for response to specific request ++ * ++ * @param int $requestId ++ * @param int $timeoutMs [optional] the number of milliseconds to wait. ++ * @return string The response content. ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception ++ */ ++ public function wait_for_response($requestId, $timeoutMs = 0) ++ { ++ return $this->wait_for_response_data($requestId, $timeoutMs)['response']; ++ } + } +diff --git a/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt b/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt +new file mode 100644 +index 00000000000..475bc130a42 +--- /dev/null ++++ b/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++FPM: GHSA-7qg2-v9fj-4mwv - status xss ++--SKIPIF-- ++<?php include "skipif.inc"; ?> ++--FILE-- ++<?php ++ ++require_once "tester.inc"; ++ ++$cfg = <<<EOT ++[global] ++error_log = {{FILE:LOG}} ++[unconfined] ++listen = {{ADDR}} ++pm = static ++pm.max_children = 2 ++pm.status_path = /status ++catch_workers_output = yes ++EOT; ++ ++$code = <<<EOT ++<?php ++usleep(200000); ++EOT; ++ ++$tester = new FPM\Tester($cfg, $code); ++$tester->start(); ++$tester->expectLogStartNotices(); ++$responses = $tester ++ ->multiRequest([ ++ ['uri' => '/<script>alert(1)</script>', 'query' => '<script>alert(2)</script>'], ++ ['uri' => '/status', 'query' => 'full&html', 'delay' => 100000], ++ ]); ++var_dump(strpos($responses[1]->getBody(), '<script>')); ++$tester->terminate(); ++$tester->expectLogTerminatingNotices(); ++$tester->close(); ++ ++?> ++Done ++--EXPECT-- ++bool(false) ++Done ++--CLEAN-- ++<?php ++require_once "tester.inc"; ++FPM\Tester::clean(); ++?> +diff --git a/sapi/fpm/tests/logtool.inc b/sapi/fpm/tests/logtool.inc +new file mode 100644 +index 00000000000..219c6fedbb8 +--- /dev/null ++++ b/sapi/fpm/tests/logtool.inc +@@ -0,0 +1,476 @@ ++<?php ++ ++namespace FPM; ++ ++class LogTool ++{ ++ const P_TIME = '\[\d\d-\w\w\w-\d{4} \d\d:\d\d:\d\d\]'; ++ const P_PREFIX = '\[pool unconfined\] child \d+ said into stderr: '; ++ const FINAL_SUFFIX = ', pipe is closed'; ++ ++ /** ++ * @var string ++ */ ++ private $message; ++ ++ /** ++ * @var string ++ */ ++ private $level; ++ ++ /** ++ * @var int ++ */ ++ private $position; ++ ++ /** ++ * @var int ++ */ ++ private $suffixPosition; ++ ++ /** ++ * @var int ++ */ ++ private $limit; ++ ++ /** ++ * @var string ++ */ ++ private $pattern; ++ ++ /** ++ * @var string ++ */ ++ private $error; ++ ++ /** ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ */ ++ public function setExpectedMessage(string $message, int $limit, int $repeat = 0) ++ { ++ $this->message = ($repeat > 0) ? str_repeat($message, $repeat) : $message; ++ $this->limit = $limit; ++ $this->position = 0; ++ } ++ ++ /** ++ * @param string $level ++ * @return int ++ */ ++ public function setExpectedLevel(string $level) ++ { ++ return $this->level = $level; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function getExpectedLevel(): string ++ { ++ return $this->level ?: 'WARNING'; ++ } ++ ++ /** ++ * @param string $line ++ * @return bool ++ */ ++ public function checkTruncatedMessage(string $line) ++ { ++ if ($this->message === null) { ++ throw new \LogicException('The message has not been set'); ++ } ++ $lineLen = strlen($line); ++ if (!$this->checkLineLength($line)) { ++ return false; ++ } ++ $this->pattern = '/^PHP message: (.*?)(\.\.\.)?$/'; ++ if (preg_match($this->pattern, $line, $matches) === 0) { ++ return $this->error("Unexpected truncated message: {$line}"); ++ } ++ ++ if ($lineLen === $this->limit) { ++ if (!isset($matches[2])) { ++ return $this->error("The truncated line is not ended with '...'"); ++ } ++ if (!$this->checkMessage($matches[1])) { ++ return false; ++ } ++ } else { ++ if (isset($matches[2])) { ++ // this is expecting that the expected message does not end with '...' ++ // which should not be an issue for the test purpose. ++ return $this->error("The line is complete and should not end with '...'"); ++ } ++ if (!$this->checkMessage($matches[1], -1)) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param array $lines ++ * @param bool $terminated ++ * @param bool $decorated ++ * @return bool ++ */ ++ public function checkWrappedMessage(array $lines, bool $terminated = true, bool $decorated = true) ++ { ++ if ($this->message === null) { ++ throw new \LogicException('The message has not been set'); ++ } ++ if ($decorated) { ++ $this->pattern = sprintf( ++ '/^(%s %s: %s)"([^"]*)"(.*)?$/', ++ self::P_TIME, ++ $this->getExpectedLevel(), ++ self::P_PREFIX ++ ); ++ } else { ++ $this->pattern = null; ++ } ++ ++ $idx = 0; ++ foreach ($lines as $idx => $line) { ++ if (!$this->checkLine($line)) { ++ break; ++ } ++ } ++ ++ if ($this->suffixPosition > 0) { ++ $suffixPattern = sprintf( ++ '/^%s %s: %s(.*)$/', ++ self::P_TIME, $this->getExpectedLevel(), ++ self::P_PREFIX ++ ); ++ $line = $lines[++$idx]; ++ if (preg_match($suffixPattern, $line, $matches) === 0) { ++ return $this->error("Unexpected line: $line"); ++ } ++ if ($matches[1] !== substr(self::FINAL_SUFFIX, $this->suffixPosition)) { ++ return $this->error( ++ "The suffix has not been finished from position $this->suffixPosition in line: $line" ++ ); ++ } ++ } ++ ++ if ($terminated) { ++ return $this->expectTerminatorLines($lines, $idx); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $line ++ * @return bool ++ */ ++ private function checkLine(string $line) ++ { ++ if ($this->pattern === null) { ++ // plain (not decorated) output ++ $out = rtrim($line); ++ $finalSuffix = null; ++ } elseif (($res = preg_match($this->pattern, $line, $matches)) > 0) { ++ $out = $matches[2]; ++ $finalSuffix = $matches[3] ?? false; ++ } else { ++ return $this->error("Unexpected line: $line"); ++ } ++ ++ $rem = strlen($this->message) - $this->position; ++ $lineLen = strlen($line); ++ if (!$this->checkLineLength($line, $lineLen)) { ++ return false; ++ } ++ if (!$this->checkMessage($out, $this->position)) { ++ return false; ++ } ++ $outLen = strlen($out); ++ if ($rem > $outLen) { // continuous line ++ if ($lineLen !== $this->limit) { ++ if ($lineLen + ($rem - $outLen) < $this->limit) { ++ return $this->error("Printed less than the message len"); ++ } ++ return $this->error( ++ "The continuous line length is $lineLen but it should equal to limit $this->limit" ++ ); ++ } ++ $this->position += $outLen; ++ return true; ++ } ++ if ($rem !== $outLen) { ++ return $this->error("Printed more than the message len"); ++ } ++ if ($finalSuffix === null || $finalSuffix === "") { ++ return false; ++ } ++ if ($finalSuffix === false) { ++ return $this->error("No final suffix"); ++ } ++ if (strpos(self::FINAL_SUFFIX, $finalSuffix) === false) { ++ return $this->error("The final suffix has to be equal to ', pipe is closed'"); ++ } ++ if (self::FINAL_SUFFIX !== $finalSuffix) { ++ $this->suffixPosition = strlen($finalSuffix); ++ } ++ // complete final suffix printed ++ return false; ++ } ++ ++ /** ++ * @param string $line ++ * @param int $lineLen ++ * @return bool ++ */ ++ private function checkLineLength(string $line, $lineLen = null) { ++ $lineLen = $lineLen ?: strlen($line); ++ if ($lineLen > $this->limit) { ++ return $this->error( ++ "The line length is $lineLen which is higher than limit $this->limit" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $matchedMessage ++ * @param int $expectedMessageStart ++ * @return bool ++ */ ++ private function checkMessage(string $matchedMessage, int $expectedMessageStart = 0) ++ { ++ if ($expectedMessageStart < 0) { ++ $expectedMessage = $this->message; ++ } else { ++ $expectedMessage = substr($this->message, $expectedMessageStart, strlen($matchedMessage)); ++ } ++ if ($expectedMessage !== $matchedMessage) { ++ return $this->error( ++ sprintf( ++ "The actual string(%d) does not match expected string(%d):\n", ++ strlen($matchedMessage), ++ strlen($expectedMessage) ++ ) . ++ "- EXPECT: '$expectedMessage'\n" . ++ "- ACTUAL: '$matchedMessage'" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param array $lines ++ * @return bool ++ */ ++ public function expectStartingLines(array $lines) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ ++ if (count($lines) < 2) { ++ return $this->error("No starting lines"); ++ } ++ ++ return ( ++ $this->expectNotice($lines[0], 'fpm is running, pid \d+') && ++ $this->expectNotice($lines[1], 'ready to handle connections') ++ ); ++ } ++ ++ /** ++ * @param array $lines ++ * @param int $idx ++ * @return bool ++ */ ++ public function expectTerminatorLines(array $lines, int $idx = -1) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ ++ if (count($lines) - $idx < 3) { ++ return $this->error("No terminating lines"); ++ } ++ ++ return ( ++ $this->expectNotice($lines[++$idx], 'Terminating ...') && ++ $this->expectNotice($lines[++$idx], 'exiting, bye-bye!') ++ ); ++ } ++ ++ /** ++ * @param string $type ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectEntry(string $type, string $line, string $expectedMessage, $pool = null) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ if ($pool !== null) { ++ $expectedMessage = '\[pool ' . $pool . '\] ' . $expectedMessage; ++ } ++ ++ $line = rtrim($line); ++ $pattern = sprintf('/^%s %s: %s$/', self::P_TIME, $type, $expectedMessage); ++ ++ if (preg_match($pattern, $line, $matches) === 0) { ++ return $this->error( ++ "The $type does not match expected message:\n" . ++ "- PATTERN: $pattern\n" . ++ "- MESSAGE: $line\n" . ++ "- EXPECT: '$expectedMessage'\n" . ++ "- ACTUAL: '" . substr($line, strpos($line, $type) + strlen($type) + 2) . "'" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectDebug(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('DEBUG', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectNotice(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('NOTICE', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectWarning(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('WARNING', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectError(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('ERROR', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectAlert(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('ALERT', $line, $expectedMessage, $pool); ++ } ++ ++ ++ /** ++ * @param string $msg ++ * @return bool ++ */ ++ private function error(string $msg) ++ { ++ $this->error = $msg; ++ echo "ERROR: $msg\n"; ++ return false; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function getError() ++ { ++ return $this->error; ++ } ++} ++ ++if (isset($argv[1]) && $argv[1] === 'logtool-selftest') { ++ $cases = [ ++ [ ++ 'limit' => 1050, ++ 'lines' => [ ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 112) . '", pipe is closed', ++ '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', ++ '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', ++ ], ++ 'message' => str_repeat('a', 2048), ++ 'type' => 'stdio', ++ ], ++ [ ++ 'limit' => 1050, ++ 'lines' => [ ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 964) . '", pi', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: pe is closed', ++ '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', ++ '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', ++ ], ++ 'message' => str_repeat('a', 2900), ++ 'type' => 'stdio', ++ ], ++ [ ++ 'limit' => 1024, ++ 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',989) . '...', ++ 'message' => str_repeat('a', 2900), ++ 'type' => 'message', ++ ], ++ [ ++ 'limit' => 1024, ++ 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',20), ++ 'message' => str_repeat('a', 20), ++ 'type' => 'message', ++ ], ++ ]; ++ foreach ($cases as $case) { ++ printf("Test message with len %d and limit %d: ", strlen($case['message']), $case['limit']); ++ $logTool = new LogTool(); ++ $logTool->setExpectedMessage($case['message'], $case['limit']); ++ if ($case['type'] === 'stdio') { ++ $logTool->checkWrappedMessage($case['lines']); ++ } else { ++ $logTool->checkTruncatedMessage($case['line']); ++ } ++ if (!$logTool->getError()) { ++ echo "OK\n"; ++ } ++ } ++ echo "Done\n"; ++} +diff --git a/sapi/fpm/tests/response.inc b/sapi/fpm/tests/response.inc +new file mode 100644 +index 00000000000..24285bf560c +--- /dev/null ++++ b/sapi/fpm/tests/response.inc +@@ -0,0 +1,281 @@ ++<?php ++ ++namespace FPM; ++ ++class Response ++{ ++ const HEADER_SEPARATOR = "\r\n\r\n"; ++ ++ /** ++ * @var array ++ */ ++ private $data; ++ ++ /** ++ * @var string ++ */ ++ private $rawData; ++ ++ /** ++ * @var string ++ */ ++ private $rawHeaders; ++ ++ /** ++ * @var string ++ */ ++ private $rawBody; ++ ++ /** ++ * @var array ++ */ ++ private $headers; ++ ++ /** ++ * @var bool ++ */ ++ private $valid; ++ ++ /** ++ * @var bool ++ */ ++ private $expectInvalid; ++ ++ /** ++ * @param string|array|null $data ++ * @param bool $expectInvalid ++ */ ++ public function __construct($data = null, $expectInvalid = false) ++ { ++ if (!is_array($data)) { ++ $data = [ ++ 'response' => $data, ++ 'err_response' => null, ++ 'out_response' => $data, ++ ]; ++ } ++ ++ $this->data = $data; ++ $this->expectInvalid = $expectInvalid; ++ } ++ ++ /** ++ * @param mixed $body ++ * @param string $contentType ++ * @return Response ++ */ ++ public function expectBody($body, $contentType = 'text/html') ++ { ++ if ($multiLine = is_array($body)) { ++ $body = implode("\n", $body); ++ } ++ ++ if ( ++ $this->checkIfValid() && ++ $this->checkDefaultHeaders($contentType) && ++ $body !== $this->rawBody ++ ) { ++ if ($multiLine) { ++ $this->error( ++ "==> The expected body:\n$body\n" . ++ "==> does not match the actual body:\n$this->rawBody" ++ ); ++ } else { ++ $this->error( ++ "The expected body '$body' does not match actual body '$this->rawBody'" ++ ); ++ } ++ } ++ ++ return $this; ++ } ++ ++ /** ++ * @return Response ++ */ ++ public function expectEmptyBody() ++ { ++ return $this->expectBody(''); ++ } ++ ++ /** ++ * @param string $contentType ++ * @return string|null ++ */ ++ public function getBody($contentType = 'text/html') ++ { ++ if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) { ++ return $this->rawBody; ++ } ++ ++ return null; ++ } ++ ++ /** ++ * Print raw body ++ */ ++ public function dumpBody() ++ { ++ var_dump($this->getBody()); ++ } ++ ++ /** ++ * Print raw body ++ */ ++ public function printBody() ++ { ++ echo $this->getBody(); ++ } ++ ++ /** ++ * Debug response output ++ */ ++ public function debugOutput() ++ { ++ echo "-------------- RESPONSE: --------------\n"; ++ echo "OUT:\n"; ++ echo $this->data['out_response']; ++ echo "ERR:\n"; ++ echo $this->data['err_response']; ++ echo "---------------------------------------\n\n"; ++ } ++ ++ /** ++ * @return string|null ++ */ ++ public function getErrorData() ++ { ++ return $this->data['err_response']; ++ } ++ ++ /** ++ * Check if the response is valid and if not emit error message ++ * ++ * @return bool ++ */ ++ private function checkIfValid() ++ { ++ if ($this->isValid()) { ++ return true; ++ } ++ ++ if (!$this->expectInvalid) { ++ $this->error("The response is invalid: $this->rawData"); ++ } ++ ++ return false; ++ } ++ ++ /** ++ * @param string $contentType ++ * @return bool ++ */ ++ private function checkDefaultHeaders($contentType) ++ { ++ // check default headers ++ return ( ++ $this->checkHeader('X-Powered-By', '|^PHP/7|', true) && ++ $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true) ++ ); ++ } ++ ++ /** ++ * @param string $name ++ * @param string $value ++ * @param bool $useRegex ++ * @return bool ++ */ ++ private function checkHeader(string $name, string $value, $useRegex = false) ++ { ++ $lcName = strtolower($name); ++ $headers = $this->getHeaders(); ++ if (!isset($headers[$lcName])) { ++ return $this->error("The header $name is not present"); ++ } ++ $header = $headers[$lcName]; ++ ++ if (!$useRegex) { ++ if ($header === $value) { ++ return true; ++ } ++ return $this->error("The header $name value '$header' is not the same as '$value'"); ++ } ++ ++ if (!preg_match($value, $header)) { ++ return $this->error("The header $name value '$header' does not match RegExp '$value'"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @return array|null ++ */ ++ private function getHeaders() ++ { ++ if (!$this->isValid()) { ++ return null; ++ } ++ ++ if (is_array($this->headers)) { ++ return $this->headers; ++ } ++ ++ $headerRows = explode("\r\n", $this->rawHeaders); ++ $headers = []; ++ foreach ($headerRows as $headerRow) { ++ $colonPosition = strpos($headerRow, ':'); ++ if ($colonPosition === false) { ++ $this->error("Invalid header row (no colon): $headerRow"); ++ } ++ $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim( ++ substr($headerRow, $colonPosition + 1) ++ ); ++ } ++ ++ return ($this->headers = $headers); ++ } ++ ++ /** ++ * @return bool ++ */ ++ private function isValid() ++ { ++ if ($this->valid === null) { ++ $this->processData(); ++ } ++ ++ return $this->valid; ++ } ++ ++ /** ++ * Process data and set validity and raw data ++ */ ++ private function processData() ++ { ++ $this->rawData = $this->data['out_response']; ++ $this->valid = ( ++ !is_null($this->rawData) && ++ strpos($this->rawData, self::HEADER_SEPARATOR) ++ ); ++ if ($this->valid) { ++ list ($this->rawHeaders, $this->rawBody) = array_map( ++ 'trim', ++ explode(self::HEADER_SEPARATOR, $this->rawData) ++ ); ++ } ++ } ++ ++ /** ++ * Emit error message ++ * ++ * @param string $message ++ * @return bool ++ */ ++ private function error($message) ++ { ++ echo "ERROR: $message\n"; ++ ++ return false; ++ } ++} +diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc +new file mode 100644 +index 00000000000..c1384133f4f +--- /dev/null ++++ b/sapi/fpm/tests/tester.inc +@@ -0,0 +1,1279 @@ ++<?php ++ ++namespace FPM; ++ ++use Adoy\FastCGI\Client; ++ ++require_once 'fcgi.inc'; ++require_once 'logtool.inc'; ++require_once 'response.inc'; ++ ++class Tester ++{ ++ /** ++ * Config directory for included files. ++ */ ++ const CONF_DIR = __DIR__ . '/conf.d'; ++ ++ /** ++ * File extension for access log. ++ */ ++ const FILE_EXT_LOG_ACC = 'acc.log'; ++ ++ /** ++ * File extension for error log. ++ */ ++ const FILE_EXT_LOG_ERR = 'err.log'; ++ ++ /** ++ * File extension for slow log. ++ */ ++ const FILE_EXT_LOG_SLOW = 'slow.log'; ++ ++ /** ++ * File extension for PID file. ++ */ ++ const FILE_EXT_PID = 'pid'; ++ ++ /** ++ * @var array ++ */ ++ static private $supportedFiles = [ ++ self::FILE_EXT_LOG_ACC, ++ self::FILE_EXT_LOG_ERR, ++ self::FILE_EXT_LOG_SLOW, ++ self::FILE_EXT_PID, ++ 'src.php', ++ 'ini', ++ 'skip.ini', ++ '*.sock', ++ ]; ++ ++ /** ++ * @var array ++ */ ++ static private $filesToClean = ['.user.ini']; ++ ++ /** ++ * @var bool ++ */ ++ private $debug; ++ ++ /** ++ * @var array ++ */ ++ private $clients; ++ ++ /** ++ * @var LogTool ++ */ ++ private $logTool; ++ ++ /** ++ * Configuration template ++ * ++ * @var string ++ */ ++ private $configTemplate; ++ ++ /** ++ * The PHP code to execute ++ * ++ * @var string ++ */ ++ private $code; ++ ++ /** ++ * @var array ++ */ ++ private $options; ++ ++ /** ++ * @var string ++ */ ++ private $fileName; ++ ++ /** ++ * @var resource ++ */ ++ private $masterProcess; ++ ++ /** ++ * @var resource ++ */ ++ private $outDesc; ++ ++ /** ++ * @var array ++ */ ++ private $ports = []; ++ ++ /** ++ * @var string ++ */ ++ private $error; ++ ++ /** ++ * The last response for the request call ++ * ++ * @var Response ++ */ ++ private $response; ++ ++ /** ++ * Clean all the created files up ++ * ++ * @param int $backTraceIndex ++ */ ++ static public function clean($backTraceIndex = 1) ++ { ++ $filePrefix = self::getCallerFileName($backTraceIndex); ++ if (substr($filePrefix, -6) === 'clean.') { ++ $filePrefix = substr($filePrefix, 0, -6); ++ } ++ ++ $filesToClean = array_merge( ++ array_map( ++ function($fileExtension) use ($filePrefix) { ++ return $filePrefix . $fileExtension; ++ }, ++ self::$supportedFiles ++ ), ++ array_map( ++ function($fileExtension) { ++ return __DIR__ . '/' . $fileExtension; ++ }, ++ self::$filesToClean ++ ) ++ ); ++ // clean all the root files ++ foreach ($filesToClean as $filePattern) { ++ foreach (glob($filePattern) as $filePath) { ++ unlink($filePath); ++ } ++ } ++ // clean config files ++ if (is_dir(self::CONF_DIR)) { ++ foreach(glob(self::CONF_DIR . '/*.conf') as $name) { ++ unlink($name); ++ } ++ rmdir(self::CONF_DIR); ++ } ++ } ++ ++ /** ++ * @param int $backTraceIndex ++ * @return string ++ */ ++ static private function getCallerFileName($backTraceIndex = 1) ++ { ++ $backtrace = debug_backtrace(); ++ if (isset($backtrace[$backTraceIndex]['file'])) { ++ $filePath = $backtrace[$backTraceIndex]['file']; ++ } else { ++ $filePath = __FILE__; ++ } ++ ++ return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION))); ++ } ++ ++ /** ++ * @return bool|string ++ */ ++ static public function findExecutable() ++ { ++ $phpPath = getenv("TEST_PHP_EXECUTABLE"); ++ for ($i = 0; $i < 2; $i++) { ++ $slashPosition = strrpos($phpPath, "/"); ++ if ($slashPosition) { ++ $phpPath = substr($phpPath, 0, $slashPosition); ++ } else { ++ break; ++ } ++ } ++ ++ if ($phpPath && is_dir($phpPath)) { ++ if (file_exists($phpPath."/fpm/php-fpm") && is_executable($phpPath."/fpm/php-fpm")) { ++ /* gotcha */ ++ return $phpPath."/fpm/php-fpm"; ++ } ++ $phpSbinFpmi = $phpPath."/sbin/php-fpm"; ++ if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) { ++ return $phpSbinFpmi; ++ } ++ } ++ ++ // try local php-fpm ++ $fpmPath = dirname(__DIR__) . '/php-fpm'; ++ if (file_exists($fpmPath) && is_executable($fpmPath)) { ++ return $fpmPath; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Skip test if any of the supplied files does not exist. ++ * ++ * @param mixed $files ++ */ ++ static public function skipIfAnyFileDoesNotExist($files) ++ { ++ if (!is_array($files)) { ++ $files = array($files); ++ } ++ foreach ($files as $file) { ++ if (!file_exists($file)) { ++ die("skip File $file does not exist"); ++ } ++ } ++ } ++ ++ /** ++ * Skip test if config file is invalid. ++ * ++ * @param string $configTemplate ++ * @throws \Exception ++ */ ++ static public function skipIfConfigFails(string $configTemplate) ++ { ++ $tester = new self($configTemplate, '', [], self::getCallerFileName()); ++ $testResult = $tester->testConfig(); ++ if ($testResult !== null) { ++ self::clean(2); ++ die("skip $testResult"); ++ } ++ } ++ ++ /** ++ * Skip test if IPv6 is not supported. ++ */ ++ static public function skipIfIPv6IsNotSupported() ++ { ++ @stream_socket_client('tcp://[::1]:0', $errno); ++ if ($errno != 111) { ++ die('skip IPv6 is not supported.'); ++ } ++ } ++ ++ /** ++ * Skip if running on Travis. ++ * ++ * @param $message ++ */ ++ static public function skipIfTravis($message) ++ { ++ if (getenv("TRAVIS")) { ++ die('skip Travis: ' . $message); ++ } ++ } ++ ++ /** ++ * Tester constructor. ++ * ++ * @param string|array $configTemplate ++ * @param string $code ++ * @param array $options ++ * @param string $fileName ++ */ ++ public function __construct( ++ $configTemplate, ++ string $code = '', ++ array $options = [], ++ $fileName = null ++ ) { ++ $this->configTemplate = $configTemplate; ++ $this->code = $code; ++ $this->options = $options; ++ $this->fileName = $fileName ?: self::getCallerFileName(); ++ $this->logTool = new LogTool(); ++ $this->debug = (bool) getenv('TEST_FPM_DEBUG'); ++ } ++ ++ /** ++ * @param string $ini ++ */ ++ public function setUserIni(string $ini) ++ { ++ $iniFile = __DIR__ . '/.user.ini'; ++ file_put_contents($iniFile, $ini); ++ } ++ ++ /** ++ * Test configuration file. ++ * ++ * @return null|string ++ * @throws \Exception ++ */ ++ public function testConfig() ++ { ++ $configFile = $this->createConfig(); ++ $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1'; ++ exec($cmd, $output, $code); ++ if ($code) { ++ return preg_replace("/\[.+?\]/", "", $output[0]); ++ } ++ ++ return null; ++ } ++ ++ /** ++ * Start PHP-FPM master process ++ * ++ * @param string $extraArgs ++ * @return bool ++ * @throws \Exception ++ */ ++ public function start(string $extraArgs = '') ++ { ++ $configFile = $this->createConfig(); ++ $desc = $this->outDesc ? [] : [1 => array('pipe', 'w')]; ++ $asRoot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : ''; ++ $cmd = self::findExecutable() . " $asRoot -F -O -y $configFile $extraArgs"; ++ /* Since it's not possible to spawn a process under linux without using a ++ * shell in php (why?!?) we need a little shell trickery, so that we can ++ * actually kill php-fpm */ ++ $this->masterProcess = proc_open( ++ "killit () { kill \$child 2> /dev/null; }; " . ++ "trap killit TERM; $cmd 2>&1 & child=\$!; wait", ++ $desc, ++ $pipes ++ ); ++ register_shutdown_function( ++ function($masterProcess) use($configFile) { ++ @unlink($configFile); ++ if (is_resource($masterProcess)) { ++ @proc_terminate($masterProcess); ++ while (proc_get_status($masterProcess)['running']) { ++ usleep(10000); ++ } ++ } ++ }, ++ $this->masterProcess ++ ); ++ if (!$this->outDesc !== false) { ++ $this->outDesc = $pipes[1]; ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Run until needle is found in the log. ++ * ++ * @param string $needle ++ * @param int $max ++ * @return bool ++ * @throws \Exception ++ */ ++ public function runTill(string $needle, $max = 10) ++ { ++ $this->start(); ++ $found = false; ++ for ($i = 0; $i < $max; $i++) { ++ $line = $this->getLogLine(); ++ if (is_null($line)) { ++ break; ++ } ++ if (preg_match($needle, $line) === 1) { ++ $found = true; ++ break; ++ } ++ } ++ $this->close(true); ++ ++ if (!$found) { ++ return $this->error("The search pattern not found"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Check if connection works. ++ * ++ * @param string $host ++ * @param null|string $successMessage ++ * @param null|string $errorMessage ++ * @param int $attempts ++ * @param int $delay ++ */ ++ public function checkConnection( ++ $host = '127.0.0.1', ++ $successMessage = null, ++ $errorMessage = 'Connection failed', ++ $attempts = 20, ++ $delay = 50000 ++ ) { ++ $i = 0; ++ do { ++ if ($i > 0 && $delay > 0) { ++ usleep($delay); ++ } ++ $fp = @fsockopen($host, $this->getPort()); ++ } while ((++$i < $attempts) && !$fp); ++ ++ if ($fp) { ++ $this->message($successMessage); ++ fclose($fp); ++ } else { ++ $this->message($errorMessage); ++ } ++ } ++ ++ ++ /** ++ * Execute request with parameters ordered for better checking. ++ * ++ * @param string $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param string $uri ++ * @param string $query ++ * @param array $headers ++ * @return Response ++ */ ++ public function checkRequest( ++ string $address, ++ string $successMessage = null, ++ string $errorMessage = null, ++ $uri = '/ping', ++ $query = '', ++ $headers = [] ++ ) { ++ return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage); ++ } ++ ++ /** ++ * Execute and check ping request. ++ * ++ * @param string $address ++ * @param string $pingPath ++ * @param string $pingResponse ++ */ ++ public function ping( ++ string $address = '{{ADDR}}', ++ string $pingResponse = 'pong', ++ string $pingPath = '/ping' ++ ) { ++ $response = $this->request('', [], $pingPath, $address); ++ $response->expectBody($pingResponse, 'text/plain'); ++ } ++ ++ /** ++ * Execute and check status request(s). ++ * ++ * @param array $expectedFields ++ * @param string|null $address ++ * @param string $statusPath ++ * @param mixed $formats ++ * @throws \Exception ++ */ ++ public function status( ++ array $expectedFields, ++ string $address = null, ++ string $statusPath = '/status', ++ $formats = ['plain', 'html', 'xml', 'json'] ++ ) { ++ if (!is_array($formats)) { ++ $formats = [$formats]; ++ } ++ ++ require_once "status.inc"; ++ $status = new Status(); ++ foreach ($formats as $format) { ++ $query = $format === 'plain' ? '' : $format; ++ $response = $this->request($query, [], $statusPath, $address); ++ $status->checkStatus($response, $expectedFields, $format); ++ } ++ } ++ ++ /** ++ * Get request params array. ++ * ++ * @param string $query ++ * @param array $headers ++ * @param string|null $uri ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return array ++ */ ++ private function getRequestParams( ++ string $query = '', ++ array $headers = [], ++ string $uri = null ++ ) { ++ if (is_null($uri)) { ++ $uri = $this->makeSourceFile(); ++ } ++ ++ $params = array_merge( ++ [ ++ 'GATEWAY_INTERFACE' => 'FastCGI/1.0', ++ 'REQUEST_METHOD' => 'GET', ++ 'SCRIPT_FILENAME' => $uri, ++ 'SCRIPT_NAME' => $uri, ++ 'QUERY_STRING' => $query, ++ 'REQUEST_URI' => $uri . ($query ? '?'.$query : ""), ++ 'DOCUMENT_URI' => $uri, ++ 'SERVER_SOFTWARE' => 'php/fcgiclient', ++ 'REMOTE_ADDR' => '127.0.0.1', ++ 'REMOTE_PORT' => '7777', ++ 'SERVER_ADDR' => '127.0.0.1', ++ 'SERVER_PORT' => '80', ++ 'SERVER_NAME' => php_uname('n'), ++ 'SERVER_PROTOCOL' => 'HTTP/1.1', ++ 'DOCUMENT_ROOT' => __DIR__, ++ 'CONTENT_TYPE' => '', ++ 'CONTENT_LENGTH' => 0 ++ ], ++ $headers ++ ); ++ ++ return array_filter($params, function($value) { ++ return !is_null($value); ++ }); ++ } ++ ++ /** ++ * Execute request. ++ * ++ * @param string $query ++ * @param array $headers ++ * @param string|null $uri ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return Response ++ */ ++ public function request( ++ string $query = '', ++ array $headers = [], ++ string $uri = null, ++ string $address = null, ++ string $successMessage = null, ++ string $errorMessage = null, ++ bool $connKeepAlive = false ++ ) { ++ if ($this->hasError()) { ++ return new Response(null, true); ++ } ++ if (is_null($uri)) { ++ $uri = $this->makeSourceFile(); ++ } ++ ++ $params = $this->getRequestParams($query, $headers, $uri); ++ ++ try { ++ $this->response = new Response( ++ $this->getClient($address, $connKeepAlive)->request_data($params, false) ++ ); ++ $this->message($successMessage); ++ } catch (\Exception $exception) { ++ if ($errorMessage === null) { ++ $this->error("Request failed", $exception); ++ } else { ++ $this->message($errorMessage); ++ } ++ $this->response = new Response(); ++ } ++ if ($this->debug) { ++ $this->response->debugOutput(); ++ } ++ return $this->response; ++ } ++ ++ /** ++ * Execute multiple requests in parallel. ++ * ++ * @param array|int $requests ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return Response[] ++ * @throws \Exception ++ */ ++ public function multiRequest( ++ $requests, ++ string $address = null, ++ string $successMessage = null, ++ string $errorMessage = null, ++ bool $connKeepAlive = false ++ ) { ++ if ($this->hasError()) { ++ return new Response(null, true); ++ } ++ ++ if (is_numeric($requests)) { ++ $requests = array_fill(0, $requests, []); ++ } elseif (!is_array($requests)) { ++ throw new \Exception('Requests can be either numeric or array'); ++ } ++ ++ try { ++ $connections = array_map(function ($requestData) use ($address, $connKeepAlive) { ++ $client = $this->getClient($address, $connKeepAlive); ++ $params = $this->getRequestParams( ++ $requestData['query'] ?? '', ++ $requestData['headers'] ?? [], ++ $requestData['uri'] ?? null ++ ); ++ return [ ++ 'client' => $client, ++ 'requestId' => $client->async_request($params, false), ++ ]; ++ }, $requests); ++ ++ $responses = array_map(function ($conn) { ++ $response = new Response($conn['client']->wait_for_response_data($conn['requestId'])); ++ if ($this->debug) { ++ $response->debugOutput(); ++ } ++ return $response; ++ }, $connections); ++ $this->message($successMessage); ++ return $responses; ++ } catch (\Exception $exception) { ++ if ($errorMessage === null) { ++ $this->error("Request failed", $exception); ++ } else { ++ $this->message($errorMessage); ++ } ++ } ++ } ++ ++ /** ++ * Get client. ++ * ++ * @param string $address ++ * @param bool $keepAlive ++ * @return Client ++ */ ++ private function getClient(string $address = null, $keepAlive = false) ++ { ++ $address = $address ? $this->processTemplate($address) : $this->getAddr(); ++ if ($address[0] === '/') { // uds ++ $host = 'unix://' . $address; ++ $port = -1; ++ } elseif ($address[0] === '[') { // ipv6 ++ $addressParts = explode(']:', $address); ++ $host = $addressParts[0]; ++ if (isset($addressParts[1])) { ++ $host .= ']'; ++ $port = $addressParts[1]; ++ } else { ++ $port = $this->getPort(); ++ } ++ } else { // ipv4 ++ $addressParts = explode(':', $address); ++ $host = $addressParts[0]; ++ $port = $addressParts[1] ?? $this->getPort(); ++ } ++ ++ if (!$keepAlive) { ++ return new Client($host, $port); ++ } ++ ++ if (!isset($this->clients[$host][$port])) { ++ $client = new Client($host, $port); ++ $client->setKeepAlive(true); ++ $this->clients[$host][$port] = $client; ++ } ++ ++ return $this->clients[$host][$port]; ++ } ++ ++ /** ++ * Display logs ++ * ++ * @param int $number ++ * @param string $ignore ++ */ ++ public function displayLog(int $number = 1, string $ignore = 'systemd') ++ { ++ /* Read $number lines or until EOF */ ++ while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { ++ $a = fgets($this->outDesc); ++ if (empty($ignore) || !strpos($a, $ignore)) { ++ echo $a; ++ $number--; ++ } ++ } ++ } ++ ++ /** ++ * Get a single log line ++ * ++ * @return null|string ++ */ ++ private function getLogLine() ++ { ++ $read = [$this->outDesc]; ++ $write = null; ++ $except = null; ++ if (stream_select($read, $write, $except, 2 )) { ++ return fgets($this->outDesc); ++ } else { ++ return null; ++ } ++ } ++ ++ /** ++ * Get log lines ++ * ++ * @param int $number ++ * @param bool $skipBlank ++ * @param string $ignore ++ * @return array ++ */ ++ public function getLogLines(int $number = 1, bool $skipBlank = false, string $ignore = 'systemd') ++ { ++ $lines = []; ++ /* Read $n lines or until EOF */ ++ while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { ++ $line = $this->getLogLine(); ++ if (is_null($line)) { ++ break; ++ } ++ if ((empty($ignore) || !strpos($line, $ignore)) && (!$skipBlank || strlen(trim($line)) > 0)) { ++ $lines[] = $line; ++ $number--; ++ } ++ } ++ ++ return $lines; ++ } ++ ++ /** ++ * @return mixed|string ++ */ ++ public function getLastLogLine() ++ { ++ $lines = $this->getLogLines(); ++ ++ return $lines[0] ?? ''; ++ } ++ ++ /** ++ * Send signal to the supplied PID or the server PID. ++ * ++ * @param string $signal ++ * @param int|null $pid ++ * @return string ++ */ ++ public function signal($signal, int $pid = null) ++ { ++ if (is_null($pid)) { ++ $pid = $this->getPid(); ++ } ++ ++ return exec("kill -$signal $pid"); ++ } ++ ++ /** ++ * Terminate master process ++ */ ++ public function terminate() ++ { ++ proc_terminate($this->masterProcess); ++ } ++ ++ /** ++ * Close all open descriptors and process resources ++ * ++ * @param bool $terminate ++ */ ++ public function close($terminate = false) ++ { ++ if ($terminate) { ++ $this->terminate(); ++ } ++ fclose($this->outDesc); ++ proc_close($this->masterProcess); ++ } ++ ++ /** ++ * Create a config file. ++ * ++ * @param string $extension ++ * @return string ++ * @throws \Exception ++ */ ++ private function createConfig($extension = 'ini') ++ { ++ if (is_array($this->configTemplate)) { ++ $configTemplates = $this->configTemplate; ++ if (!isset($configTemplates['main'])) { ++ throw new \Exception('The config template array has to have main config'); ++ } ++ $mainTemplate = $configTemplates['main']; ++ unset($configTemplates['main']); ++ if (!is_dir(self::CONF_DIR)) { ++ mkdir(self::CONF_DIR); ++ } ++ foreach ($configTemplates as $name => $configTemplate) { ++ $this->makeFile( ++ 'conf', ++ $this->processTemplate($configTemplate), ++ self::CONF_DIR, ++ $name ++ ); ++ } ++ } else { ++ $mainTemplate = $this->configTemplate; ++ } ++ ++ return $this->makeFile($extension, $this->processTemplate($mainTemplate)); ++ } ++ ++ /** ++ * Process template string. ++ * ++ * @param string $template ++ * @return string ++ */ ++ private function processTemplate(string $template) ++ { ++ $vars = [ ++ 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC], ++ 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR], ++ 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW], ++ 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID], ++ 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC], ++ 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR], ++ 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW], ++ 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID], ++ 'ADDR:IPv4' => ['getAddr', 'ipv4'], ++ 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'], ++ 'ADDR:IPv6' => ['getAddr', 'ipv6'], ++ 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'], ++ 'ADDR:UDS' => ['getAddr', 'uds'], ++ 'PORT' => ['getPort', 'ip'], ++ 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf', ++ ]; ++ $aliases = [ ++ 'ADDR' => 'ADDR:IPv4', ++ 'FILE:LOG' => 'FILE:LOG:ERR', ++ ]; ++ foreach ($aliases as $aliasName => $aliasValue) { ++ $vars[$aliasName] = $vars[$aliasValue]; ++ } ++ ++ return preg_replace_callback( ++ '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/', ++ function ($matches) use ($vars) { ++ $varName = $matches[1]; ++ if (!isset($vars[$varName])) { ++ $this->error("Invalid config variable $varName"); ++ return 'INVALID'; ++ } ++ $pool = $matches[2] ?? 'default'; ++ $varValue = $vars[$varName]; ++ if (is_string($varValue)) { ++ return $varValue; ++ } ++ $functionName = array_shift($varValue); ++ $varValue[] = $pool; ++ return call_user_func_array([$this, $functionName], $varValue); ++ }, ++ $template ++ ); ++ } ++ ++ /** ++ * @param string $type ++ * @param string $pool ++ * @return string ++ */ ++ public function getAddr(string $type = 'ipv4', $pool = 'default') ++ { ++ $port = $this->getPort($type, $pool, true); ++ if ($type === 'uds') { ++ return $this->getFile($port . '.sock'); ++ } ++ ++ return $this->getHost($type) . ':' . $port; ++ } ++ ++ /** ++ * @param string $type ++ * @param string $pool ++ * @param bool $useAsId ++ * @return int ++ */ ++ public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false) ++ { ++ if ($type === 'uds' && !$useAsId) { ++ return -1; ++ } ++ ++ if (isset($this->ports['values'][$pool])) { ++ return $this->ports['values'][$pool]; ++ } ++ $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1; ++ $this->ports['values'][$pool] = $this->ports['last'] = $port; ++ ++ return $port; ++ } ++ ++ /** ++ * @param string $type ++ * @return string ++ */ ++ public function getHost(string $type = 'ipv4') ++ { ++ switch ($type) { ++ case 'ipv6-any': ++ return '[::]'; ++ case 'ipv6': ++ return '[::1]'; ++ case 'ipv4-any': ++ return '0.0.0.0'; ++ default: ++ return '127.0.0.1'; ++ } ++ } ++ ++ /** ++ * Get listen address. ++ * ++ * @param string|null $template ++ * @return string ++ */ ++ public function getListen($template = null) ++ { ++ return $template ? $this->processTemplate($template) : $this->getAddr(); ++ } ++ ++ /** ++ * Get PID. ++ * ++ * @return int ++ */ ++ public function getPid() ++ { ++ $pidFile = $this->getFile('pid'); ++ if (!is_file($pidFile)) { ++ return (int) $this->error("PID file has not been created"); ++ } ++ $pidContent = file_get_contents($pidFile); ++ if (!is_numeric($pidContent)) { ++ return (int) $this->error("PID content '$pidContent' is not integer"); ++ } ++ ++ return (int) $pidContent; ++ } ++ ++ ++ /** ++ * @param string $extension ++ * @param string|null $dir ++ * @param string|null $name ++ * @return string ++ */ ++ private function getFile(string $extension, $dir = null, $name = null) ++ { ++ $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension; ++ ++ return is_null($dir) ? $fileName : $dir . '/' . $fileName; ++ } ++ ++ /** ++ * @param string $extension ++ * @return string ++ */ ++ private function getAbsoluteFile(string $extension) ++ { ++ return $this->getFile($extension); ++ } ++ ++ /** ++ * @param string $extension ++ * @return string ++ */ ++ private function getRelativeFile(string $extension) ++ { ++ $fileName = rtrim(basename($this->fileName), '.'); ++ ++ return $this->getFile($extension, null, $fileName); ++ } ++ ++ /** ++ * @param string $extension ++ * @param string $prefix ++ * @return string ++ */ ++ private function getPrefixedFile(string $extension, string $prefix = null) ++ { ++ $fileName = rtrim($this->fileName, '.'); ++ if (!is_null($prefix)) { ++ $fileName = $prefix . '/' . basename($fileName); ++ } ++ ++ return $this->getFile($extension, null, $fileName); ++ } ++ ++ /** ++ * @param string $extension ++ * @param string $content ++ * @param string|null $dir ++ * @param string|null $name ++ * @return string ++ */ ++ private function makeFile(string $extension, string $content = '', $dir = null, $name = null) ++ { ++ $filePath = $this->getFile($extension, $dir, $name); ++ file_put_contents($filePath, $content); ++ ++ return $filePath; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function makeSourceFile() ++ { ++ return $this->makeFile('src.php', $this->code); ++ } ++ ++ /** ++ * @param string|null $msg ++ */ ++ private function message($msg) ++ { ++ if ($msg !== null) { ++ echo "$msg\n"; ++ } ++ } ++ ++ /** ++ * @param string $msg ++ * @param \Exception|null $exception ++ */ ++ private function error($msg, \Exception $exception = null) ++ { ++ $this->error = 'ERROR: ' . $msg; ++ if ($exception) { ++ $this->error .= '; EXCEPTION: ' . $exception->getMessage(); ++ } ++ $this->error .= "\n"; ++ ++ echo $this->error; ++ } ++ ++ /** ++ * @return bool ++ */ ++ private function hasError() ++ { ++ return !is_null($this->error) || !is_null($this->logTool->getError()); ++ } ++ ++ /** ++ * Expect file with a supplied extension to exist. ++ * ++ * @param string $extension ++ * @param string $prefix ++ * @return bool ++ */ ++ public function expectFile(string $extension, $prefix = null) ++ { ++ $filePath = $this->getPrefixedFile($extension, $prefix); ++ if (!file_exists($filePath)) { ++ return $this->error("The file $filePath does not exist"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Expect file with a supplied extension to not exist. ++ * ++ * @param string $extension ++ * @param string $prefix ++ * @return bool ++ */ ++ public function expectNoFile(string $extension, $prefix = null) ++ { ++ $filePath = $this->getPrefixedFile($extension, $prefix); ++ if (file_exists($filePath)) { ++ return $this->error("The file $filePath exists"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Expect message to be written to FastCGI error stream. ++ * ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ */ ++ public function expectFastCGIErrorMessage( ++ string $message, ++ int $limit = 1024, ++ int $repeat = 0 ++ ) { ++ $this->logTool->setExpectedMessage($message, $limit, $repeat); ++ $this->logTool->checkTruncatedMessage($this->response->getErrorData()); ++ } ++ ++ /** ++ * Expect starting lines to be logged. ++ */ ++ public function expectLogStartNotices() ++ { ++ $this->logTool->expectStartingLines($this->getLogLines(2)); ++ } ++ ++ /** ++ * Expect terminating lines to be logged. ++ */ ++ public function expectLogTerminatingNotices() ++ { ++ $this->logTool->expectTerminatorLines($this->getLogLines(-1)); ++ } ++ ++ /** ++ * Expect log message that can span multiple lines. ++ * ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ * @param bool $decorated ++ * @param bool $wrapped ++ */ ++ public function expectLogMessage( ++ string $message, ++ int $limit = 1024, ++ int $repeat = 0, ++ bool $decorated = true, ++ bool $wrapped = true ++ ) { ++ $this->logTool->setExpectedMessage($message, $limit, $repeat); ++ if ($wrapped) { ++ $logLines = $this->getLogLines(-1, true); ++ $this->logTool->checkWrappedMessage($logLines, true, $decorated); ++ } else { ++ $logLines = $this->getLogLines(1, true); ++ $this->logTool->checkTruncatedMessage($logLines[0] ?? ''); ++ } ++ if ($this->debug) { ++ $this->message("-------------- LOG LINES: -------------"); ++ var_dump($logLines); ++ $this->message("---------------------------------------\n"); ++ } ++ } ++ ++ /** ++ * Expect a single log line. ++ * ++ * @param string $message ++ * @return bool ++ */ ++ public function expectLogLine(string $message) ++ { ++ $messageLen = strlen($message); ++ $limit = $messageLen > 1024 ? $messageLen + 16 : 1024; ++ $this->logTool->setExpectedMessage($message, $limit); ++ $logLines = $this->getLogLines(1, true); ++ if ($this->debug) { ++ $this->message("LOG LINE: " . ($logLines[0] ?? '')); ++ } ++ ++ return $this->logTool->checkWrappedMessage($logLines, false); ++ } ++ ++ /** ++ * Expect a log debug message. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogDebug(string $message, $pool = null) ++ { ++ return $this->logTool->expectDebug($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log notice. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogNotice(string $message, $pool = null) ++ { ++ return $this->logTool->expectNotice($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log warning. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogWarning(string $message, $pool = null) ++ { ++ return $this->logTool->expectWarning($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log error. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogError(string $message, $pool = null) ++ { ++ return $this->logTool->expectError($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log alert. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogAlert(string $message, $pool = null) ++ { ++ return $this->logTool->expectAlert($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect no log lines to be logged. ++ * ++ * @return bool ++ */ ++ public function expectNoLogMessages() ++ { ++ $logLines = $this->getLogLines(-1, true); ++ if (!empty($logLines)) { ++ return $this->error( ++ "Expected no log lines but following lines logged:\n" . implode("\n", $logLines) ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Print content of access log. ++ */ ++ public function printAccessLog() ++ { ++ $accessLog = $this->getFile('acc.log'); ++ if (is_file($accessLog)) { ++ print file_get_contents($accessLog); ++ } ++ } ++} +-- +2.54.0 + +From 3ef1cb1937145019ef836cba3a5736a37384c752 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 7 May 2026 09:01:35 +0200 +Subject: [PATCH 6/6] NEWS from 8.2.31 + +(cherry picked from commit 7dff10e9a31d469fcd436e10b06f8b2bf2758a68) +(cherry picked from commit 1cbf0c27044bd54fb77de8a6bf993a7ab53892a4) +(cherry picked from commit 6b9f5d1673522bb3cf5d77889919084024565c7f) +(cherry picked from commit 5be222339cd6d299aa9170e6fa9edd51a5c42f39) +(cherry picked from commit 8884e113e8351693eb4b5f1c58485ad0e4508d3a) +(cherry picked from commit 5cf6ff5fcde53a1a941fea374b483e9ff89a9f9f) +--- + NEWS | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +diff --git a/NEWS b/NEWS +index 7096b787738..b5014af12e4 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,24 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.2.31 ++ ++- FPM: ++ . Fixed GHSA-7qg2-v9fj-4mwv (XSS within status endpoint). (CVE-2026-6735) ++ (Jakub Zelenka) ++ ++- SOAP: ++ . Fixed GHSA-85c2-q967-79q5 (Stale SOAP_GLOBAL(ref_map) pointer with Apache ++ Map). (CVE-2026-6722) (ilutov) ++ . Fixed GHSA-m33r-qmcv-p97q (Use-after-free after header parsing failure with ++ SOAP_PERSISTENCE_SESSION). (CVE-2026-7261) (ilutov) ++ . Fixed GHSA-hmxp-6pc4-f3vv (Broken Apache map value NULL check). ++ (CVE-2026-7262) (ilutov) ++ ++- Standard: ++ . Fixed GHSA-96wq-48vp-hh57 (Signed integer overflow of char array offset). ++ (CVE-2026-7568) (TimWolla) ++ + Backported from 8.1.31 + + - CLI: +-- +2.54.0 + diff --git a/php-cve-2026-7261.patch b/php-cve-2026-7261.patch new file mode 100644 index 0000000..1d4e8ae --- /dev/null +++ b/php-cve-2026-7261.patch @@ -0,0 +1,122 @@ +From f91ab4e04bc2f254ea1e49e1b76ff55adbbe3892 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sun, 3 May 2026 19:57:16 +0200 +Subject: [PATCH 2/6] GHSA-m33r-qmcv-p97q: [soap] Fix use-after-free after + header parsing failure with SOAP_PERSISTENCE_SESSION + +Fixes GHSA-m33r-qmcv-p97q +Fixes CVE-2026-7261 + +(cherry picked from commit db2a7f9348fd5dda5fd162061786a664c417bf5b) +(cherry picked from commit 5dd8dd8493d49bb6fcd810a6e9d2ffb6fdc15714) +(cherry picked from commit 63cf032e9675d7d2bbc007c8c787597187a7567b) +(cherry picked from commit dd14d36e31dd99b7589f917924840fe4f46ca022) +(cherry picked from commit 7b354983a33c314b76c594c9c5b790e3b073dcf1) + +adapt test for 7.2 + +(cherry picked from commit f91bcf961ac15eacabf33f86f62c17dbec4a39ab) +(cherry picked from commit ab6fa685773d4efea4de2df4956c97ffd65637e2) +--- + ext/soap/soap.c | 12 ++++- + ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt | 60 +++++++++++++++++++++++++ + 2 files changed, 70 insertions(+), 2 deletions(-) + create mode 100644 ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt + +diff --git a/ext/soap/soap.c b/ext/soap/soap.c +index 62b119fb2bf..e436c278760 100644 +--- a/ext/soap/soap.c ++++ b/ext/soap/soap.c +@@ -1839,13 +1839,21 @@ PHP_METHOD(SoapServer, handle) + php_output_discard(); + soap_server_fault_ex(function, &h->retval, h); + efree(fn_name); +- if (service->type == SOAP_CLASS && soap_obj) {zval_ptr_dtor(soap_obj);} ++ if (service->type == SOAP_CLASS && soap_obj) { ++ if (service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) { ++ zval_ptr_dtor(soap_obj); ++ } ++ } + goto fail; + } else if (EG(exception)) { + php_output_discard(); + _soap_server_exception(service, function, getThis()); + efree(fn_name); +- if (service->type == SOAP_CLASS && soap_obj) {zval_ptr_dtor(soap_obj);} ++ if (service->type == SOAP_CLASS && soap_obj) { ++ if (service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) { ++ zval_ptr_dtor(soap_obj); ++ } ++ } + goto fail; + } + } else if (h->mustUnderstand) { +diff --git a/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt b/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt +new file mode 100644 +index 00000000000..6e4e9e75fb6 +--- /dev/null ++++ b/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-m33r-qmcv-p97q: Use-after-free after header parsing failure with SOAP_PERSISTENCE_SESSION ++--CREDITS-- ++Ilia Alshanetsky (iliaal) ++--EXTENSIONS-- ++soap ++session ++--FILE-- ++<?php ++ ++class Handler { ++ public function return() { ++ return new SoapFault('Server', 'denied'); ++ } ++ public function throw() { ++ throw new SoapFault('Server', 'denied'); ++ } ++ public function hello() { ++ return 'ok'; ++ } ++} ++ ++session_start(); ++ ++$srv = new SoapServer(null, ['uri' => 'urn:a']); ++$srv->setClass(Handler::class); ++$srv->setPersistence(SOAP_PERSISTENCE_SESSION); ++ ++$x = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="urn:a"> ++ <soap:Header> ++ <a:return/> ++ </soap:Header> ++ <soap:Body> ++ <a:hello/> ++ </soap:Body> ++</soap:Envelope> ++XML; ++$srv->handle($x); ++ ++$x = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="urn:a"> ++ <soap:Header> ++ <a:throw/> ++ </soap:Header> ++ <soap:Body> ++ <a:hello/> ++ </soap:Body> ++</soap:Envelope> ++XML; ++$srv->handle($x); ++ ++?> ++--EXPECT-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>denied</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>denied</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> +-- +2.54.0 + diff --git a/php-cve-2026-7262.patch b/php-cve-2026-7262.patch new file mode 100644 index 0000000..625989f --- /dev/null +++ b/php-cve-2026-7262.patch @@ -0,0 +1,83 @@ +From b1bc3b191eb9ff6ca90f90572ba8fac016163fe9 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sat, 25 Apr 2026 00:44:37 +0200 +Subject: [PATCH 3/6] GHSA-hmxp-6pc4-f3vv: [soap] Fix broken Apache map value + NULL check + +Fixes GHSA-hmxp-6pc4-f3vv +Fixes CVE-2026-7262 + +(cherry picked from commit 79551ab8b1a97760c739e372f9bc359619f3554d) +(cherry picked from commit aed3e63e282235b32a07ca28cc20728eedfcfec3) +(cherry picked from commit 8c897384b867a573d52a04b455fe2da30671d0ea) +(cherry picked from commit b41a11a9786cc5b6b343b47c37ad8c1fdc2dbf33) +(cherry picked from commit 254773b5b1d0ef25409c35e74b87c5ef93459115) +(cherry picked from commit c21561700dcfc3304322845c2d3da028c3c73345) +(cherry picked from commit 16c2b25d363d73d72a3139e747cc9d5c8d5bef2b) +--- + ext/soap/php_encoding.c | 2 +- + ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt | 39 +++++++++++++++++++++++++ + 2 files changed, 40 insertions(+), 1 deletion(-) + create mode 100644 ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt + +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index 40fba95980a..d88dba76228 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -2757,7 +2757,7 @@ static zval *to_zval_map(zval *ret, encodeTypePtr type, xmlNodePtr data) + } + + xmlValue = get_node(item->children, "value"); +- if (!xmlKey) { ++ if (!xmlValue) { + soap_error0(E_ERROR, "Encoding: Can't decode apache map, missing value"); + } + +diff --git a/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt b/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt +new file mode 100644 +index 00000000000..e46ab2e4607 +--- /dev/null ++++ b/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt +@@ -0,0 +1,39 @@ ++--TEST-- ++GHSA-hmxp-6pc4-f3vv: Null pointer dereference on missing Apache map value ++--CREDITS-- ++Ilia Alshanetsky (iliaal) ++--EXTENSIONS-- ++soap ++--FILE-- ++<?php ++ ++$request = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope ++ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" ++ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ++ xmlns:xsd="http://www.w3.org/2001/XMLSchema" ++ xmlns:apache="http://xml.apache.org/xml-soap"> ++ ++ <soap:Body> ++ <test> ++ <map xsi:type="apache:Map"> ++ <item><key>hello</key></item> ++ </map> ++ </test> ++ </soap:Body> ++</soap:Envelope> ++XML; ++ ++$server = new SoapServer(null, [ ++ 'uri' => 'urn:test', ++ 'typemap' => [['type_name' => 'anything']], ++]); ++$server->addFunction('test'); ++function test($m) { return null; } ++$server->handle($request); ++ ++?> ++--EXPECT-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>SOAP-ERROR: Encoding: Can't decode apache map, missing value</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> +-- +2.54.0 + diff --git a/php-cve-2026-7568.patch b/php-cve-2026-7568.patch new file mode 100644 index 0000000..f3e7186 --- /dev/null +++ b/php-cve-2026-7568.patch @@ -0,0 +1,90 @@ +From 99eec43bb407d42855eaa9ff6af64df1ee2c20dc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@tideways-gmbh.com> +Date: Sun, 3 May 2026 20:02:57 +0200 +Subject: [PATCH 4/6] GHSA-96wq-48vp-hh57: [metaphone] Fix signed integer + overflow of char array offset + +Fixes GHSA-96wq-48vp-hh57 +Fixes CVE-2026-7568 + +(cherry picked from commit 47def8ce1db1fdbffcfc1f5bb11877a0e22d4b32) +(cherry picked from commit e4fc187a011d91f26178f6dfbccdb07041b99153) +(cherry picked from commit 53de456406a6db5a8bcded8a4b242789ae5b2690) +(cherry picked from commit 909c2acc64d72bd57123b30e711c02aef0c08d14) + +[skip ci] Adjust credits for GHSA-96wq-48vp-hh57.phpt + +As requested by the reporter. + +(cherry picked from commit fee84dd8c7699e4e7f9b2e864a393ee5a372f974) +(cherry picked from commit 101e93900888ef43d42ec0e33866bca3824f51a8) +(cherry picked from commit 41134d0746a524d7265b67d3d8d0fd433fd7479a) +(cherry picked from commit b40b656c0fe8080f9cd097bf77b7a3681ea3e7a0) +(cherry picked from commit 9e4b7c856c57deda7b7887da7978328ec8b57187) +(cherry picked from commit b7702525bc4a540eb36f392a13461971a1bac31a) +(cherry picked from commit b6affc4bc51768aec7ad8737f4486597939b0bd4) +--- + ext/standard/metaphone.c | 8 ++++---- + ext/standard/tests/GHSA-96wq-48vp-hh57.phpt | 22 +++++++++++++++++++++ + 2 files changed, 26 insertions(+), 4 deletions(-) + create mode 100644 ext/standard/tests/GHSA-96wq-48vp-hh57.phpt + +diff --git a/ext/standard/metaphone.c b/ext/standard/metaphone.c +index 9bf67bbda89..23ebf144e76 100644 +--- a/ext/standard/metaphone.c ++++ b/ext/standard/metaphone.c +@@ -122,10 +122,10 @@ char _codes[26] = + + /* Allows us to safely look ahead an arbitrary # of letters */ + /* I probably could have just used strlen... */ +-static char Lookahead(char *word, int how_far) ++static char Lookahead(char *word, size_t how_far) + { + char letter_ahead = '\0'; /* null by default */ +- int idx; ++ size_t idx; + for (idx = 0; word[idx] != '\0' && idx < how_far; idx++); + /* Edge forward in the string... */ + +@@ -167,8 +167,8 @@ static char Lookahead(char *word, int how_far) + */ + static int metaphone(unsigned char *word, size_t word_len, zend_long max_phonemes, zend_string **phoned_word, int traditional) + { +- int w_idx = 0; /* point in the phonization we're at. */ +- int p_idx = 0; /* end of the phoned phrase */ ++ size_t w_idx = 0; /* point in the phonization we're at. */ ++ size_t p_idx = 0; /* end of the phoned phrase */ + size_t max_buffer_len = 0; /* maximum length of the destination buffer */ + + /*-- Parameter checks --*/ +diff --git a/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt +new file mode 100644 +index 00000000000..cf9a40062f8 +--- /dev/null ++++ b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt +@@ -0,0 +1,22 @@ ++--TEST-- ++GHSA-96wq-48vp-hh57: signed integer overflow of char array offset ++--CREDITS-- ++Aleksey Solovev (Positive Technologies) ++--INI-- ++memory_limit=3G ++--SKIPIF-- ++<?php ++if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test'); ++if (getenv('SKIP_SLOW_TESTS')) die('skip slow test'); ++if (PHP_INT_SIZE != 8) echo 'skip 64-bit only'; ++?> ++--FILE-- ++<?php ++ ++$str = str_repeat('0', 2 * (1024 ** 3) - 2) . 'AE'; ++metaphone($str, 1); ++ ++?> ++===DONE=== ++--EXPECT-- ++===DONE=== +-- +2.54.0 + diff --git a/php-fpm.service b/php-fpm.service index 687dfc0..0712a11 100644 --- a/php-fpm.service +++ b/php-fpm.service @@ -4,7 +4,7 @@ [Unit] Description=The PHP FastCGI Process Manager -After=syslog.target network.target +After=network.target [Service] Type=notify @@ -61,7 +61,7 @@ %global oraclelib 19.1 %global oracledir 19.24 %else -%global oraclever 23.6 +%global oraclever 23.26.2 %global oraclemax 24 %global oraclelib 23.1 %global oracledir 23 @@ -136,7 +136,7 @@ Summary: PHP scripting language for creating dynamic web sites Name: %{?scl_prefix}php Version: %{upver}%{?rcver:~%{rcver}} -Release: 45%{?dist} +Release: 46%{?dist} # All files licensed under PHP version 3.01, except # Zend is licensed under Zend # TSRM is licensed under BSD @@ -279,6 +279,12 @@ Patch273: php-cve-2024-11234.patch Patch274: php-cve-2024-8932.patch Patch275: php-cve-2024-11233.patch Patch276: php-ghsa-4w77-75f9-2c8w.patch +# from 8.2.31 +Patch277: php-cve-2026-6722.patch +Patch278: php-cve-2026-7261.patch +Patch279: php-cve-2026-7262.patch +Patch280: php-cve-2026-6735.patch +Patch281: php-cve-2026-7568.patch # Fixes for tests (300+) # Factory is droped from system tzdata @@ -1102,6 +1108,11 @@ sed -e 's/php-devel/%{?scl_prefix}php-devel/' -i scripts/phpize.in %patch -P274 -p1 -b .cve8932 %patch -P275 -p1 -b .cve11233 %patch -P276 -p1 -b .ghsa4w77 +%patch -P277 -p1 -b .cve6722 +%patch -P278 -p1 -b .cve7261 +%patch -P279 -p1 -b .cve7262 +%patch -P280 -p1 -b .cve6735 +%patch -P281 -p1 -b .cve7268 # Fixes for tests %patch -P300 -p1 -b .datetests @@ -1873,7 +1884,7 @@ cat << EOF WARNING : PHP 7.0 have reached its "End of Life" in December 2018. Even, if this package includes some of - the important security fixes, backported from 8.1, the + the important security fixes, backported from 8.2, the UPGRADE to a maintained version is very strongly RECOMMENDED. ===================================================================== @@ -2057,6 +2068,18 @@ EOF %changelog +* Tue May 12 2026 Remi Collet <remi@remirepo.net> - 7.0.33-46 +- Fix XSS within status endpoint + CVE-2026-6735 +- Fix Stale SOAP_GLOBAL(ref_map) pointer with Apache Map + CVE-2026-6722 +- Fix Use-after-free after header parsing failure with SOAP_PERSISTENCE_SESSION + CVE-2026-7261 +- Fix Broken Apache map value NULL check + CVE-2026-7262 +- Fix Signed integer overflow of char array offset + CVE-2026-7568 + * Tue Nov 26 2024 Remi Collet <remi@remirepo.net> - 7.0.33-45 - Fix Heap-Use-After-Free in sapi_read_post_data Processing in CLI SAPI Interface GHSA-4w77-75f9-2c8w |
