Commit d822e940a22 for php.net
commit d822e940a2273cd8fdfa9d7cf9680e3aa1d83a46
Author: Ilija Tovilo <ilija.tovilo@me.com>
Date: Tue Jun 16 14:42:12 2026 +0200
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
diff --git a/NEWS b/NEWS
index 6ba018dab89..ec9efcbe02a 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 00000000000..9044b1bbdbe
--- /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--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_supplemental_read.pem.tmp';
+$peerName = 'crypto-status-supplemental-read';
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_supplemental_read.pem.tmp');
+?>
+--EXPECT--
+hello
+bool(true)
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 92a17b60c5c..93a9971b4a7 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);