[php-src] PHP-8.4: Fix TLS stream EOF detection after close_notify with stale errno (#24132)

From: Date: Mon, 05 Oct 2026 09:35:08 +0000
Subject: [php-src] PHP-8.4: Fix TLS stream EOF detection after close_notify with stale errno (#24132)
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-141950@lists.php.net to get a copy of this message
Author: Jakub Zelenka (bukka) Committer: GitHub (web-flow) Pusher: bukka Date: 2026-10-05T11:35:05+02:00 Commit: https://github.com/php/php-src/commit/614c62872ac48cf04ac674f9f44633c01750a3b6 Raw diff: https://github.com/php/php-src/commit/614c62872ac48cf04ac674f9f44633c01750a3b6.diff Fix TLS stream EOF detection after close_notify with stale errno (#24132) php_openssl_handle_ssl_error() sets errno to EAGAIN on SSL_ERROR_WANT_READ and SSL_ERROR_WANT_WRITE, and php_openssl_sockop_io() reads it back to avoid marking a non-blocking read that needs to wait as EOF. OpenSSL resets errno before every recv() on POSIX systems, but on Windows it uses the Winsock error state instead and never touches errno, so the EAGAIN stays set across every later successful read. A close_notify received afterwards returned SSL_ERROR_ZERO_RETURN but did not set stream->eof, and feof() stayed false while the TCP connection was still open. Decide EOF from the SSL error code, which already says whether the operation just needs to wait, instead of from errno. Changed paths: A ext/openssl/tests/stream_eof_after_close_notify.phpt M NEWS M ext/openssl/xp_ssl.c Diff: diff --git a/NEWS b/NEWS index 40393c15fb76..91ed81e4018a 100644 --- a/NEWS +++ b/NEWS @@ -120,6 +120,8 @@ PHP NEWS - OpenSSL: . Fixed stream_socket_enable_crypto() leaving the socket non-blocking after a handshake timeout. (Ilia Alshanetsky) + . Fixed feof() on a TLS stream staying false after a close_notify on Windows. + (Jakub Zelenka) - PCNTL: . Fixed pcntl_signal_dispatch() dropping the queued signals when it runs while diff --git a/ext/openssl/tests/stream_eof_after_close_notify.phpt b/ext/openssl/tests/stream_eof_after_close_notify.phpt new file mode 100644 index 000000000000..4755e171544d --- /dev/null +++ b/ext/openssl/tests/stream_eof_after_close_notify.phpt @@ -0,0 +1,58 @@ +--TEST-- +feof() is true after a TLS close_notify even if an earlier read left errno set to EAGAIN +--EXTENSIONS-- +openssl +--SKIPIF-- +<?php +if (!function_exists("proc_open")) die("skip no proc_open"); +?> +--FILE-- +<?php +$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_eof_after_close_notify.pem.tmp'; + +$serverCode = <<<'CODE' + $serverCtx = stream_context_create(['ssl' => ['local_cert' => '%s']]); + $sock = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx); + phpt_notify_server_start($sock); + + $link = stream_socket_accept($sock); + /* Let the client block in fread() first, so its SSL_read() sees WANT_READ */ + phpt_wait(); + usleep(100000); + fwrite($link, "data"); + /* close_notify only, the TCP connection stays open */ + stream_socket_enable_crypto($link, false); + phpt_wait(); + fclose($link); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]]); + $sock = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 2, STREAM_CLIENT_CONNECT, $clientCtx); + + phpt_notify(); + var_dump(fread($sock, 4)); + var_dump(fread($sock, 4)); + var_dump(feof($sock)); + phpt_notify(); +CODE; + +include 'CertificateGenerator.inc'; +(new CertificateGenerator())->saveNewCertAsFileWithKey('stream_eof_after_close_notify', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- +<?php +@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'stream_eof_after_close_notify.pem.tmp'); +?> +--EXPECT-- +string(4) "data" +string(0) "" +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index b564be8cad53..125b22423806 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2186,9 +2186,11 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si retry = 1; } - /* Also, on reads, we may get this condition on an EOF. We should check properly. */ if (read) { - stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle)); + /* EOF unless the SSL layer just needs to wait. */ + stream->eof = (retry == 0 + && err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE + && !SSL_pending(sslsock->ssl_handle)); } /* Don't loop indefinitely in non-blocking mode if no data is available */

« previous php.cvs (#141950) next »