From fc0f25f286104e367c3b8be99cb2b42fc268dcae Mon Sep 17 00:00:00 2001 From: Nora Dossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:52:14 +0100 Subject: [PATCH 1/5] Fix NULL deref when enabling TLS fails and the peer name needs to be reset The code tries to read the context on NULL when `php_stream_xport_crypto_setup` fails because by then `stream` is reset to NULL. This is also UB, so can cause miscompiles. ``` ==1217==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000090 (pc 0x55d829ed3acf bp 0x7fff045f5770 sp 0x7fff045f4df0 T0) ==1217==The signal is caused by a READ memory access. ==1217==Hint: address points to the zero page. #0 0x55d829ed3acf in php_stream_url_wrap_http_ex /work/php-src/ext/standard/http_fopen_wrapper.c:580 #1 0x55d829ed857e in php_stream_url_wrap_http /work/php-src/ext/standard/http_fopen_wrapper.c:1204 #2 0x55d82a15073d in _php_stream_open_wrapper_ex /work/php-src/main/streams/streams.c:2270 #3 0x55d829e78fa6 in zif_file_get_contents /work/php-src/ext/standard/file.c:409 #4 0x55d829bbfe39 in zif_phar_file_get_contents /work/php-src/ext/phar/func_interceptors.c:226 #5 0x55d82a0b7ed2 in zend_test_execute_internal /work/php-src/ext/zend_test/observer.c:306 #6 0x55d82a3e024a in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER /work/php-src/Zend/zend_vm_execute.h:2154 #7 0x55d82a540995 in execute_ex /work/php-src/Zend/zend_vm_execute.h:116519 #8 0x55d82a5558b0 in zend_execute /work/php-src/Zend/zend_vm_execute.h:121962 #9 0x55d82a6ba0ab in zend_execute_script /work/php-src/Zend/zend.c:1980 #10 0x55d82a0ec8bb in php_execute_script_ex /work/php-src/main/main.c:2645 #11 0x55d82a0ecccb in php_execute_script /work/php-src/main/main.c:2685 #12 0x55d82a6bfc16 in do_cli /work/php-src/sapi/cli/php_cli.c:951 #13 0x55d82a6c21e3 in main /work/php-src/sapi/cli/php_cli.c:1362 #14 0x7f9e770491c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e) #15 0x7f9e7704928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 274eec488d230825a136fa9c4d85370fed7a0a5e) #16 0x55d829209b34 in _start (/work/php-src/build-dbg-asan/sapi/cli/php+0x609b34) (BuildId: aa149f943514fff0c491e1f199e30fed0e977f7c) ``` This is a backport of: * 7782b8876bc0f61b0e8bcce262e22896e423b22e * f9519ccbb9975b2afae95a418fae65ad93823dc6 * afded3dffc432a86955e6b8018146abb3d03779b Co-authored-by: Ilija Tovilo --- NEWS | 4 ++ ext/openssl/tests/gh21031.phpt | 55 ++++++++++++++++++++ ext/openssl/tests/sni_server_cs_expired.pem | 57 +++++++++++++++++++++ ext/standard/http_fopen_wrapper.c | 12 +++-- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 ext/openssl/tests/gh21031.phpt create mode 100644 ext/openssl/tests/sni_server_cs_expired.pem diff --git a/NEWS b/NEWS index 9a9d86351752..9e0f80a15ccc 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.3.32 +- Streams: + . Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL + and a proxy set). (CVE-2026-12184) (ndossche) + - OpenSSL: . Fixed bug GH-22187 (Memory corruption (zend_mm_heap corrupted) in openssl_encrypt with AES-WRAP-PAD). (David Carlier) diff --git a/ext/openssl/tests/gh21031.phpt b/ext/openssl/tests/gh21031.phpt new file mode 100644 index 000000000000..25bdb385a791 --- /dev/null +++ b/ext/openssl/tests/gh21031.phpt @@ -0,0 +1,55 @@ +--TEST-- +GH-21031 (Fix NULL deref when enabling TLS fails and the peer name needs to be reset) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'SNI_server_certs' => [ + "cs.php.net" => __DIR__ . "/sni_server_cs_expired.pem", + ] + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $serverFlags, $ctx); + phpt_notify_server_start($server); + + $conn = stream_socket_accept($server, 10); + if ($conn) { + fclose($conn); + } + + phpt_wait(); +CODE; + +$clientCode = <<<'CODE' + $clientCtx = stream_context_create([ + 'ssl' => [ + 'cafile' => __DIR__ . '/sni_server_ca.pem', + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + "http" => [ + "proxy" => "tcp://{{ ADDR }}" + ], + ]); + + var_dump(@file_get_contents("https://cs.php.net/", false, $clientCtx)); + + phpt_notify(); +CODE; + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +bool(false) diff --git a/ext/openssl/tests/sni_server_cs_expired.pem b/ext/openssl/tests/sni_server_cs_expired.pem new file mode 100644 index 000000000000..9f5a201b26d1 --- /dev/null +++ b/ext/openssl/tests/sni_server_cs_expired.pem @@ -0,0 +1,57 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvy5NhzktzEdsHTbGB6vqYANms5rn1zXFmTJrGlWCwoIsNmTf +ahvZkrC1cCXTZ7fbPB8XQbpAtz2ZSU7OcwBW9B8okYUPo9zi/ptwcrgsQsN0hrcD +8MBRUccevwime5fLvg8E9RJ/68y9y3BnRcVWYO2sAK9juTfidNjETU3Bb05oXv8D +SD/6onXQu4uXDgsQ3cRXeld9UB0xazmQXyyiIqXc/cpTAnaEVYzn28aj7NlUbzNq +511UXMXY44x9EcXWpPVZ7heNcJNzY5DCNzmtXKrt9yiMpWQcPXEzsESVxAMqib9u +TFOlvVX17LIPxBG656PjTD9J1h6kBbMCUxzs7wIDAQABAoIBAQC85lBeY0X4ST3v +I7bJz7kWQ2YP4uhfAdeLhoDDFWjNLffniwYhfwEc6xNri0R2f/jUT9gX7qORKwEx +qPdeNCC2t67LElGg1FlJv2Z9Q7MgCKYzkdQH5s6y4e9kTHTLO/JpiceZKz1QTQ3f +XOH9032E6nIAf0wmr6xHTgOwajrN8VI5BuPEMVmEwIw3AtYeqVuPCNKyGR4HUVkC +2bAydnGngbRJRnNzmKcWJancxpHDGBSFqPyuXMFC7Jgo3ZmyCbGp99vuXVk/sW9x +5aj94M9nRE0guk05ivH2/JZao2uLYkIgjFWlhNxKdWgWRk8DEuN4djC8mKS9YH1q +crYRToMhAoGBAOspUTtKP54mpZmyhxuDqj02JaJRzNTskPHsiF1UhtXuw7uT+ryV +ekUFLNXoFmn9mbx1WVaUvGH4qjilvQOxz7u++lz0ApqJEfyM3jc/cC40Y5zcuGSu +Etbg+SyDoytlgMCIydJyrS7NNALSo5p5oG6XY2f8yd/DCAmo8LzypaHRAoGBANAf +R1SlBMc/bOsi6GrJxcBVSCFMiKYiO5woL5aUKa9yM+UQuQ/6xbQ7Q+sOlt0FH3xo +AJ2L60qTdjyXVtjOdtXs5ZC4l+C6AfnCx6yLr+fNc4SOYXEfqS4LZylgwKd9KyVB +asspIW9Idbgebmi6vPyt9LDkIp0h1VuFGjkvQJK/AoGBAI4pbS0dprXyARyYW6sb +fpgAmuG099IkrT9DUfCx/81myTclr2fAKal+BmvOIXaz0/OlMXvw8K19iVIzh7+r +B70lJ+93p/dKM/BsLI5TsHqOO0YB/QsIXOVAHgJ2FfdPJnW+e9vYba+kZ/Po6PSi +4ITaykJ8BIJcQgis89QWEGFxAoGBAJhQO+jzuDKF9ZWEf6ofrw0anOZZ16wWY5/e +PS2rk3JmVxpuibHrKqPDt+ogTELHDAsFJmYmz3VNxHuFmrajK49Wh4/JuMVr/CQo +6+8YcA1qa/94IFIlBLDBAafjujsZvOjQHnM+z8xcsGKmStF00Pjv6qNG4xoyd646 +FD4DmfOLAoGAWXehpopZKXE9gRAni881ucK6WqxPPBoofbozi09D0MmfarIVaSkv +jNVVHBfLWd7IEXTjiipPBeUqq6Jc3pscN1Vp4rrl8jTmVTdazEv0LuzpdUFqmNo2 +M+xw17uz9D9Q32/aW1Lar0PdIaL/wGEDEyzEBFwrGppcENLilPz8gzU= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIFIjCCAwqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwVTELMAkGA1UEBhMCR0Ix +EDAOBgNVBAgMB0VuZ2xhbmQxEDAOBgNVBAoMB1BIUC5uZXQxEDAOBgNVBAsMB29w +ZW5zc2wxEDAOBgNVBAMMB3BocC5uZXQwHhcNMTgwMTE0MTgzNjEyWhcNMjYwNDAy +MTgzNjEyWjBGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDEQMA4GA1UE +CgwHUEhQLm5ldDETMBEGA1UEAwwKY3MucGhwLm5ldDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAL8uTYc5LcxHbB02xger6mADZrOa59c1xZkyaxpVgsKC +LDZk32ob2ZKwtXAl02e32zwfF0G6QLc9mUlOznMAVvQfKJGFD6Pc4v6bcHK4LELD +dIa3A/DAUVHHHr8IpnuXy74PBPUSf+vMvctwZ0XFVmDtrACvY7k34nTYxE1NwW9O +aF7/A0g/+qJ10LuLlw4LEN3EV3pXfVAdMWs5kF8soiKl3P3KUwJ2hFWM59vGo+zZ +VG8zauddVFzF2OOMfRHF1qT1We4XjXCTc2OQwjc5rVyq7fcojKVkHD1xM7BElcQD +Kom/bkxTpb1V9eyyD8QRuuej40w/SdYepAWzAlMc7O8CAwEAAaOCAQkwggEFMAkG +A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVu +U1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFHPfd8dK +Lz1R0Ck4WV1B9AWXd5DSMGwGA1UdIwRlMGOAFOPK44Eacedv7HbR2Igcbew+4kUa +oUekRTBDMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDEQMA4GA1UECgwH +UEhQLm5ldDEQMA4GA1UEAwwHcGhwLm5ldIICEAAwDgYDVR0PAQH/BAQDAgWgMBMG +A1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4ICAQB6WSIHEyDXLZxH +hZjqSNQOA7Wc9Z2FCAiD29xYkGTL8WuPVGGP1mu4B92ytj+PMWwqSReDa7eTGLE7 +O7ozw9l+c+gNmHFNikSsGjlV2E8CToQOFMny+jAQYMSXf8UbTp9xDfgG02t/71hv +SLWqdeHMLcR0xi0nBQH0vDOkwUbuWYqFa3jejHieGhykHM6CkIk6lqnyOEO+ooIF +ZsLprrg1ss/mXCPI6niP0hze55ERKdxI7Rk8sZ4pVkf2SUWqZrUS0aJ+Ymmwi6Xd +2V7izq5N30PkJS8MtqII4FAjRBIkwPh0sy8PmW/DzkYU+lYQnDfYLKDFKcj8xJK/ +o8oZUBsQltrSj0KlM9QuqxCTCBCy1nXZ9WHOhq+jdLiTc1Oi60uEHcUMrLK8aYc4 +HqIvZS6C2iwMI0d1OP3VxmAbMQ9yqRi+FbLYavJ3H40jrU9SYqdxa0BrTaz8MJNE +6AEwgQDPChczSghvHME+Fs4mtGCY3TesbNZKVahQRjaFIhMZIZ4RP4CRc0bJOBG+ +8Me4+KHNsD2ki5b03wAN6C1P2QrMzI+gH9fXLZYp761ciDAsX6YIzrhHHYLxYpJH +BkQKKs8dCQWE5IzgVrdlvC3Z1/l9om66wHqqx7nKnPfYs/Sfnwe9MpCD6xJrXiTm +WS7NM6fbQpO9APNr7o0ZOjbbWFzlNw== +-----END CERTIFICATE----- diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 040ee4eabf78..00f7b4dfa80e 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -546,6 +546,10 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, smart_str_appendl(&header, "\r\n", sizeof("\r\n")-1); if (php_stream_write(stream, ZSTR_VAL(header.s), ZSTR_LEN(header.s)) != ZSTR_LEN(header.s)) { + if (reset_ssl_peer_name) { + php_stream_context_unset_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name"); + } + php_stream_wrapper_log_error(wrapper, options, "Cannot connect to HTTPS server through proxy"); php_stream_close(stream); stream = NULL; @@ -567,16 +571,18 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, /* enable SSL transport layer */ if (stream) { + php_stream_context *old_context = PHP_STREAM_CONTEXT(stream); + if (php_stream_xport_crypto_setup(stream, STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 || php_stream_xport_crypto_enable(stream, 1) < 0) { php_stream_wrapper_log_error(wrapper, options, "Cannot connect to HTTPS server through proxy"); php_stream_close(stream); stream = NULL; } - } - if (reset_ssl_peer_name) { - php_stream_context_unset_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name"); + if (reset_ssl_peer_name) { + php_stream_context_unset_option(old_context, "ssl", "peer_name"); + } } } From d6c3f4b5466d96cfd7fac6e9a9a6959347224546 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 1 Jul 2026 11:18:18 +0200 Subject: [PATCH 2/5] PHP-8.3 is now for PHP 8.3.33-dev --- NEWS | 5 ++++- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index 9e0f80a15ccc..348d062e83a4 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.3.32 +?? ??? ????, PHP 8.3.33 + + +02 Jul 2026, PHP 8.3.32 - Streams: . Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL diff --git a/Zend/zend.h b/Zend/zend.h index aa8787d844b2..e022b97a663d 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.3.31-dev" +#define ZEND_VERSION "4.3.33-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index b79f91ddecdd..f85650811327 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.3.31-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.3.33-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index 21bea7bb26a2..df3ccaf05352 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 3 -#define PHP_RELEASE_VERSION 31 +#define PHP_RELEASE_VERSION 33 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.3.31-dev" -#define PHP_VERSION_ID 80331 +#define PHP_VERSION "8.3.33-dev" +#define PHP_VERSION_ID 80333 From 1c196d0ee5670b0e9873fad49dda5464b8294707 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 17 Jun 2026 00:19:31 +0200 Subject: [PATCH 3/5] Fixed timeout for supplemental read at end of a blocking stream in SSL stream wrapper This issue is related to GH-22332: > Let's assume we have a stream with 5 queued bytes. When calling fread($s, 1), > the underlying buffered stream will request a chunk of up to 8192 bytes > immediately, but only return the requested 1 byte back to the reader. If the > reader then requests fread($s, 10), _php_stream_read() will first return > anything that was buffered but not yet read. > > If the requested length exceeds the number of buffered bytes (as is the case > above), another read call is issued. This call will return nothing, because > the stream only provides 4 more readable bytes, all of which are buffered. > php_openssl_handle_ssl_error() (called by php_openssl_sockop_io()) will then > incorrectly set last_status to WANT_READ, even though we've already read the > remaining data. > > Furthermore, stream_select() can cause the same issue via > php_openssl_sockop_cast(castas: PHP_STREAM_AS_FD_FOR_SELECT), which pre-fills > the read buffer on SSL_pending() > 0. The subsequent fread() will lead to the > same condition as above. > > There's a second issue here. If the stream is blocking, the supplement read > will block for the duration of the timeout. This will be addressed in a second > PR. This addresses the last paragraph. Avoid a blocking read when we already have buffered data, as we may have reached the end of the stream and will wait in vain. Closes GH-22346 --- NEWS | 4 + .../stream_supplemental_read_timeout.phpt | 90 +++++++++++++++++++ ext/openssl/xp_ssl.c | 12 ++- 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 ext/openssl/tests/stream_supplemental_read_timeout.phpt diff --git a/NEWS b/NEWS index d03a77b01d91..feb9221c2e0b 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,10 @@ PHP NEWS . Fixed IntlChar methods leaving stale global error state after successful calls. (Xuyang Zhang) +- OpenSSL: + . Fixed timeout for supplemental read at end of a blocking stream in SSL + stream wrapper. (ilutov) + - PDO_ODBC: . Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN carries no credentials). (iliaal) diff --git a/ext/openssl/tests/stream_supplemental_read_timeout.phpt b/ext/openssl/tests/stream_supplemental_read_timeout.phpt new file mode 100644 index 000000000000..0e36f146bf9e --- /dev/null +++ b/ext/openssl/tests/stream_supplemental_read_timeout.phpt @@ -0,0 +1,90 @@ +--TEST-- +Timeout for supplemental read at end of a blocking stream in SSL stream wrapper +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + ['local_cert' => '%s']]); + $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; + $server = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $conn = stream_socket_accept($server, 30); + + fwrite($conn, "hello\n"); + + phpt_wait(); + fclose($conn); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'peer_name' => '%s', + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx); + stream_set_blocking($client, true); + stream_set_timeout($client, 5); + $start = hrtime(true); + + $buf = ''; + $read = [$client]; + $write = $except = null; + while (true) { + if (!stream_select($read, $write, $except, 5)) { + break; + } + + // Initially, read only the first char, then request more than is stored + // in the buffer, triggering a supplemental read. + $chunk = fread($client, strlen($buf) === 0 ? 1 : 10); + if ($chunk === '' || $chunk === false) { + /* A non-application record (e.g. a TLS 1.3 session ticket) may arrive first. */ + if (feof($client)) { + break; + } + } else { + $buf .= $chunk; + if (strlen($buf) >= 6) { + break; + } + } + $read = [$client]; + $write = $except = null; + } + + echo trim($buf), "\n"; + + $diff = (hrtime(true) - $start) / 1e9; + var_dump($diff < 4.0); + + phpt_notify(); + fclose($client); +CODE; +$clientCode = sprintf($clientCode, $peerName); + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +hello +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index ffacd8a107b7..77c98f65b518 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2069,7 +2069,11 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si /* Only do this if SSL is active. */ if (sslsock->ssl_active) { - int retry = 1; + /* We have already returned some buffered data. Don't retry and don't + * block. We're just trying to fill the buffer more, but the stream might + * be empty, so we don't want to wait in vain. */ + bool supplemental = stream->has_buffered_data; + int retry = !supplemental; struct timeval start_time; struct timeval *timeout = NULL; int began_blocked = sslsock->s.is_blocked; @@ -2082,11 +2086,11 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } /* never use a timeout with non-blocking sockets */ - if (began_blocked) { + if (began_blocked && !supplemental) { timeout = &sslsock->s.timeout; } - if (timeout) { + if (timeout || supplemental) { php_openssl_set_blocking(sslsock, 0); } @@ -2160,7 +2164,7 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } /* Don't loop indefinitely in non-blocking mode if no data is available */ - if (began_blocked == 0) { + if (began_blocked == 0 || supplemental) { break; } From d822e940a2273cd8fdfa9d7cf9680e3aa1d83a46 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 16 Jun 2026 14:42:12 +0200 Subject: [PATCH 4/5] Fix stream_socket_get_crypto_status() after supplemental read Let's assume we have a stream with 5 queued bytes. When calling fread($s, 1), the underlying buffered stream will request a chunk of up to 8192 bytes immediately, but only return the requested 1 byte back to the reader. If the reader then requests fread($s, 10), _php_stream_read() will first return anything that was buffered but not yet read. If the requested length exceeds the number of buffered bytes (as is the case above), another read call is issued. This call will return nothing, because the stream only provides 4 more readable bytes, all of which are buffered. php_openssl_handle_ssl_error() (called by php_openssl_sockop_io()) will then incorrectly set last_status to WANT_READ, even though we've already read the remaining data. Furthermore, stream_select() can cause the same issue via php_openssl_sockop_cast(castas: PHP_STREAM_AS_FD_FOR_SELECT), which pre-fills the read buffer on SSL_pending() > 0. The subsequent fread() will lead to the same condition as above. Closes GH-22332 --- NEWS | 2 + ...t_get_crypto_status_supplemental_read.phpt | 83 +++++++++++++++++++ ext/openssl/xp_ssl.c | 10 +++ 3 files changed, 95 insertions(+) create mode 100644 ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt diff --git a/NEWS b/NEWS index 6ba018dab89d..ec9efcbe02a3 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0alpha2 +- OpenSSL: + . Fixed stream_socket_get_crypto_status() after supplemental read. (ilutov) 02 Jul 2026, PHP 8.6.0alpha1 diff --git a/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt b/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt new file mode 100644 index 000000000000..9044b1bbdbe4 --- /dev/null +++ b/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt @@ -0,0 +1,83 @@ +--TEST-- +stream_socket_get_crypto_status(): reports status NONE after supplemental read +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + ['local_cert' => '%s']]); + $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; + $server = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $conn = stream_socket_accept($server, 30); + + fwrite($conn, "hello\n"); + + phpt_wait(); + fclose($conn); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'peer_name' => '%s', + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx); + stream_set_blocking($client, false); + + $buf = ''; + $read = [$client]; + $write = $except = null; + while (stream_select($read, $write, $except, 5)) { + // Initially, read only the first char, then request more than is stored + // in the buffer, triggering a supplemental read. + $chunk = fread($client, strlen($buf) === 0 ? 1 : 10); + if ($chunk === '' || $chunk === false) { + /* A non-application record (e.g. a TLS 1.3 session ticket) may arrive first. */ + if (feof($client)) { + break; + } + } else { + $buf .= $chunk; + if (strlen($buf) >= 6) { + break; + } + } + $read = [$client]; + $write = $except = null; + } + + echo trim($buf), "\n"; + /* A successful read clears the pending status back to NONE. */ + var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_NONE); + + phpt_notify(); + fclose($client); +CODE; +$clientCode = sprintf($clientCode, $peerName); + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +hello +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 92a17b60c5ce..93a9971b4a78 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3019,6 +3019,16 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), nr_bytes, 0); } + /* This might be a supplemental read after consuming buffered data. If + * the read returned nothing, ignore status WANT_READ. */ + if (read && + supplemental && + nr_bytes <= 0 && + sslsock->last_status == STREAM_CRYPTO_STATUS_WANT_READ + ) { + sslsock->last_status = STREAM_CRYPTO_STATUS_NONE; + } + /* And if we were originally supposed to be blocking, let's reset the socket to that. */ if (began_blocked) { php_openssl_set_blocking(sslsock, 1); From d27da095b1cbc353887dda31f165c0ff45de5a05 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 17 Apr 2026 11:19:26 -0400 Subject: [PATCH 5/5] Fix infinite recursion in property hook getter in opcache preloaded trait preload_fix_trait_op_array rewrites the clone op_array from its original after optimization, preserving function_name, scope, fn_flags, prototype, and static_variables. For trait-cloned property hooks, it also needs to preserve prop_info: zend_do_traits_property_binding set it to the using class's property, but the reset pointed it back at the trait's property. That mismatch caused zend_is_in_hook to miss the self-access check inside the hook, recursing into the getter/setter instead of reading or writing the backing store. Fixes GH-21770 Closes GH-21788 --- NEWS | 2 ++ ext/opcache/ZendAccelerator.c | 2 ++ ext/opcache/tests/gh21770.phpt | 25 +++++++++++++++++++++++++ ext/opcache/tests/preload_gh21770.inc | 20 ++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 ext/opcache/tests/gh21770.phpt create mode 100644 ext/opcache/tests/preload_gh21770.inc diff --git a/NEWS b/NEWS index feb9221c2e0b..012a28ffe6ce 100644 --- a/NEWS +++ b/NEWS @@ -231,6 +231,8 @@ PHP NEWS . Fixed bug GH-21593 (Borked function JIT JMPNZ smart branch). (ilutov) . Fixed bug GH-21460 (COND optimization regression). (Dmitry, Arnaud) . Fixed faulty returns out of zend_try block in zend_jit_trace(). (ilutov) + . Fixed bug GH-21770 (Infinite recursion in property hook getter in opcache + preloaded trait). (iliaal) - OpenSSL: . Fix a bunch of memory leaks and crashes on edge cases. (ndossche) diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index dca5f607ad39..a2d964c15070 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4290,12 +4290,14 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) uint32_t fn_flags = op_array->fn_flags; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; + const zend_property_info *prop_info = op_array->prop_info; *op_array = *orig_op_array; op_array->function_name = function_name; op_array->scope = scope; op_array->fn_flags = fn_flags; op_array->prototype = prototype; op_array->static_variables = ht; + op_array->prop_info = prop_info; } static void preload_fix_trait_methods(zend_class_entry *ce) diff --git a/ext/opcache/tests/gh21770.phpt b/ext/opcache/tests/gh21770.phpt new file mode 100644 index 000000000000..d2fd52a4712b --- /dev/null +++ b/ext/opcache/tests/gh21770.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-21770 (Infinite recursion in property hook getter in opcache preloaded trait) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_gh21770.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +a, "\n"; + +$c = new C(); +$c->x = 42; +var_dump($c->x); +?> +--EXPECT-- +a +int(42) diff --git a/ext/opcache/tests/preload_gh21770.inc b/ext/opcache/tests/preload_gh21770.inc new file mode 100644 index 000000000000..7433a15eca92 --- /dev/null +++ b/ext/opcache/tests/preload_gh21770.inc @@ -0,0 +1,20 @@ + $this->a; + } +} + +trait X { + public int $x = 0 { + set(int $value) => $value; + } +} + +class B { + use A; +} + +class C { + use X; +}