Commit 88161995562 for php.net
commit 881619955620811c57ea31a9fd34cb04467d065a
Author: Jakub Zelenka <bukka@php.net>
Date: Thu Jun 4 19:02:11 2026 +0200
Add stream crypto status for exposing OSSL WANT_READ / WANT_WRITE (#22193)
On a non-blocking stream, stream_socket_enable_crypto() returns 0 and
fread()/fwrite() return an empty result when the TLS engine needs more
I/O, but there was no way to tell whether OpenSSL was waiting to read or
to write. Callers therefore could not reliably decide which direction to
poll for with stream_select(), which is required to drive a non-blocking
handshake or renegotiation correctly (e.g. SSL_read() wanting a write).
This tracks the last SSL_ERROR_WANT_READ/WANT_WRITE on the stream and
exposes it via a new stream_socket_get_crypto_status() function and three
constants:
STREAM_CRYPTO_STATUS_NONE
STREAM_CRYPTO_STATUS_WANT_READ
STREAM_CRYPTO_STATUS_WANT_WRITE
The status is updated during the handshake (php_openssl_enable_crypto())
and during reads/writes (php_openssl_sockop_io()), reset to NONE before
each operation, and retrieved through a new STREAM_XPORT_CRYPTO_OP_GET_STATUS
transport op. It is meaningful immediately after an operation that
returned 0/false on a non-blocking stream; a completed operation reports
NONE.
Tests cover the status during a non-blocking handshake, a non-blocking
read with no application data pending, and the constant values.
diff --git a/NEWS b/NEWS
index 8e5da57950a..496b61ef81b 100644
--- a/NEWS
+++ b/NEWS
@@ -113,6 +113,8 @@ PHP NEWS
and Openssl\Session class. (Jakub Zelenka)
. Added TLS external PSK support for streams with new context options and
Openssl\Psk class. (Jakub Zelenka)
+ . Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE.
+ (Jakub Zelenka)
- PCNTL:
. pcntl_exec() now throws a ValueError if the $args array is not a list
diff --git a/UPGRADING b/UPGRADING
index 35f8276adc3..679fc0b552d 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -323,6 +323,7 @@ PHP 8.6 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/clamp_v2
. `stream_last_errors()` and `stream_clear_errors()`.
RFC: https://wiki.php.net/rfc/stream_errors
+ . stream_socket_get_crypto_status()
- Zip:
. Added ZipArchive::openString() method.
@@ -389,6 +390,9 @@ PHP 8.6 UPGRADE NOTES
- Standard
. ARRAY_FILTER_USE_VALUE.
+ . STREAM_CRYPTO_STATUS_NONE
+ . STREAM_CRYPTO_STATUS_WANT_READ
+ . STREAM_CRYPTO_STATUS_WANT_WRITE
========================================
11. Changes to INI File Handling
diff --git a/ext/openssl/tests/stream_socket_get_crypto_status_basic.phpt b/ext/openssl/tests/stream_socket_get_crypto_status_basic.phpt
new file mode 100644
index 00000000000..b659eee79e7
--- /dev/null
+++ b/ext/openssl/tests/stream_socket_get_crypto_status_basic.phpt
@@ -0,0 +1,22 @@
+--TEST--
+stream_socket_get_crypto_status(): constants and behavior on a non-crypto stream
+--EXTENSIONS--
+openssl
+--FILE--
+<?php
+/* The status constants. */
+var_dump(STREAM_CRYPTO_STATUS_NONE);
+var_dump(STREAM_CRYPTO_STATUS_WANT_READ);
+var_dump(STREAM_CRYPTO_STATUS_WANT_WRITE);
+
+/* A plain (non-SSL) socket has no pending crypto operation, so the status
+ * is STREAM_CRYPTO_STATUS_NONE. */
+$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+var_dump(@stream_socket_get_crypto_status($server) === STREAM_CRYPTO_STATUS_NONE);
+fclose($server);
+?>
+--EXPECT--
+int(0)
+int(1)
+int(2)
+bool(true)
diff --git a/ext/openssl/tests/stream_socket_get_crypto_status_handshake.phpt b/ext/openssl/tests/stream_socket_get_crypto_status_handshake.phpt
new file mode 100644
index 00000000000..2a1c554a79d
--- /dev/null
+++ b/ext/openssl/tests/stream_socket_get_crypto_status_handshake.phpt
@@ -0,0 +1,97 @@
+--TEST--
+stream_socket_get_crypto_status(): reports WANT_READ/WANT_WRITE during a non-blocking handshake
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_handshake.pem.tmp';
+$peerName = 'crypto-status-handshake';
+
+/* Plain blocking TLS server. */
+$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);
+ if ($conn) {
+ fwrite($conn, "ok\n");
+ phpt_wait();
+ fclose($conn);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+/* Client connects over plain TCP, then completes the TLS handshake in non-blocking mode, using
+ * the reported crypto status to select the right direction to wait on. */
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'peer_name' => '%s',
+ ]]);
+
+ $client = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx);
+ stream_set_blocking($client, false);
+
+ $sawWant = false;
+ $pendingAlwaysWant = true;
+
+ do {
+ $r = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+ if ($r === 0) {
+ $status = stream_socket_get_crypto_status($client);
+ if ($status === STREAM_CRYPTO_STATUS_WANT_READ
+ || $status === STREAM_CRYPTO_STATUS_WANT_WRITE) {
+ $sawWant = true;
+ } else {
+ /* must never be NONE while the handshake is still pending */
+ $pendingAlwaysWant = false;
+ }
+
+ /* Wait on the direction the engine actually asked for. */
+ $read = $write = $except = null;
+ if ($status === STREAM_CRYPTO_STATUS_WANT_WRITE) {
+ $write = [$client];
+ } else {
+ $read = [$client];
+ }
+ stream_select($read, $write, $except, 1);
+ }
+ } while ($r === 0);
+
+ var_dump($r);
+ var_dump($sawWant);
+ var_dump($pendingAlwaysWant);
+ /* After a completed handshake the status is reset to NONE. */
+ var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_NONE);
+
+ stream_set_blocking($client, true);
+ echo trim(fgets($client)), "\n";
+ 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_handshake.pem.tmp');
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+ok
diff --git a/ext/openssl/tests/stream_socket_get_crypto_status_read.phpt b/ext/openssl/tests/stream_socket_get_crypto_status_read.phpt
new file mode 100644
index 00000000000..308950a8ef0
--- /dev/null
+++ b/ext/openssl/tests/stream_socket_get_crypto_status_read.phpt
@@ -0,0 +1,95 @@
+--TEST--
+stream_socket_get_crypto_status(): reports WANT_READ on a non-blocking read with no application data
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_read.pem.tmp';
+$peerName = 'crypto-status-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);
+
+ /* Do not send anything until the client has performed its first read, so that the read is
+ * guaranteed to find no application data. */
+ phpt_wait();
+ 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);
+
+ /* No application data has been sent yet - a non-blocking read returns nothing and the crypto
+ * status reflects that the OpenSSL wants to read. */
+ $data = fread($client, 100);
+ var_dump($data === '' || $data === false);
+ var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_WANT_READ);
+
+ /* Now let the server send. */
+ phpt_notify();
+
+ $buf = '';
+ $read = [$client];
+ $write = $except = null;
+ while (stream_select($read, $write, $except, 5)) {
+ $chunk = fread($client, 100);
+ 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_read.pem.tmp');
+?>
+--EXPECT--
+bool(true)
+bool(true)
+hello
+bool(true)
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 9486513fab7..a154aa4572f 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -207,6 +207,7 @@ typedef struct _php_openssl_netstream_data_t {
int enable_on_connect;
int is_client;
int ssl_active;
+ int last_status;
php_stream_xport_crypt_method_t method;
php_openssl_handshake_bucket_t *reneg;
php_openssl_sni_cert_t *sni_certs;
@@ -271,6 +272,10 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i
* packets: retry in next iteration */
errno = EAGAIN;
retry = is_init ? true : sslsock->s.is_blocked;
+ if (!retry) {
+ sslsock->last_status = err == SSL_ERROR_WANT_READ ?
+ STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE;
+ }
break;
case SSL_ERROR_SYSCALL:
if (ERR_peek_error() == 0) {
@@ -2701,6 +2706,8 @@ static int php_openssl_enable_crypto(php_stream *stream,
int cert_captured = 0;
X509 *peer_cert;
+ sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
+
if (cparam->inputs.activate && !sslsock->ssl_active) {
struct timeval start_time, *timeout;
bool blocked = sslsock->s.is_blocked, has_timeout = false;
@@ -2900,6 +2907,7 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si
/* Now, do the IO operation. Don't block if we can't complete... */
ERR_clear_error();
+ sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
if (read) {
nr_bytes = SSL_read(sslsock->ssl_handle, buf, (int)count);
@@ -2974,6 +2982,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si
php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
(POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL);
}
+ } else if (err == SSL_ERROR_WANT_READ) {
+ sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ;
+ } else if (err == SSL_ERROR_WANT_WRITE) {
+ sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE;
}
}
@@ -3445,6 +3457,9 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
case STREAM_XPORT_CRYPTO_OP_ENABLE:
cparam->outputs.returncode = php_openssl_enable_crypto(stream, sslsock, cparam);
return PHP_STREAM_OPTION_RETURN_OK;
+ case STREAM_XPORT_CRYPTO_OP_GET_STATUS:
+ cparam->outputs.returncode = sslsock->last_status;
+ return PHP_STREAM_OPTION_RETURN_OK;
default:
/* fall through */
break;
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index 7f7e0de0f65..7fc31e7d80c 100644
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -3484,6 +3484,11 @@ function stream_socket_sendto($socket, string $data, int $flags = 0, string $add
*/
function stream_socket_enable_crypto($stream, bool $enable, ?int $crypto_method = null, $session_stream = null): int|bool {}
+/**
+ * @param resource $stream
+ */
+function stream_socket_get_crypto_status($stream): int {}
+
#ifdef HAVE_SHUTDOWN
/** @param resource $stream */
function stream_socket_shutdown($stream, int $mode): bool {}
diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h
index a17efc6961d..c5266c5a877 100644
Binary files a/ext/standard/basic_functions_arginfo.h and b/ext/standard/basic_functions_arginfo.h differ
diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h
index 067959c8b5f..630a4b7e656 100644
Binary files a/ext/standard/basic_functions_decl.h and b/ext/standard/basic_functions_decl.h differ
diff --git a/ext/standard/file.stub.php b/ext/standard/file.stub.php
index 5e12c43f397..d7b1fef17cd 100644
--- a/ext/standard/file.stub.php
+++ b/ext/standard/file.stub.php
@@ -256,6 +256,23 @@
*/
const STREAM_CRYPTO_PROTO_TLSv1_3 = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_STATUS_NONE
+ */
+const STREAM_CRYPTO_STATUS_NONE = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_STATUS_WANT_READ
+ */
+const STREAM_CRYPTO_STATUS_WANT_READ = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_STATUS_WANT_WRITE
+ */
+const STREAM_CRYPTO_STATUS_WANT_WRITE = UNKNOWN;
+
+
/**
* @var int
* @cvalue STREAM_SHUT_RD
diff --git a/ext/standard/file_arginfo.h b/ext/standard/file_arginfo.h
index b3888925ee9..24e3722cd86 100644
Binary files a/ext/standard/file_arginfo.h and b/ext/standard/file_arginfo.h differ
diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c
index 3609ee1f3f2..bad645f8668 100644
--- a/ext/standard/streamsfuncs.c
+++ b/ext/standard/streamsfuncs.c
@@ -1638,6 +1638,18 @@ PHP_FUNCTION(stream_socket_enable_crypto)
}
/* }}} */
+/* Get crypto status */
+PHP_FUNCTION(stream_socket_get_crypto_status)
+{
+ php_stream *stream;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ PHP_Z_PARAM_STREAM(stream)
+ ZEND_PARSE_PARAMETERS_END();
+
+ RETURN_LONG(php_stream_xport_crypto_get_status(stream));
+}
+
/* {{{ Determine what file will be opened by calls to fopen() with a relative path */
PHP_FUNCTION(stream_resolve_include_path)
{
diff --git a/main/streams/php_stream_transport.h b/main/streams/php_stream_transport.h
index 0125035aaa6..60bea8e9e1f 100644
--- a/main/streams/php_stream_transport.h
+++ b/main/streams/php_stream_transport.h
@@ -186,11 +186,17 @@ typedef enum {
STREAM_CRYPTO_METHOD_ANY_SERVER = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6))
} php_stream_xport_crypt_method_t;
+/* Flags for crypto status */
+#define STREAM_CRYPTO_STATUS_NONE 0
+#define STREAM_CRYPTO_STATUS_WANT_READ 1
+#define STREAM_CRYPTO_STATUS_WANT_WRITE 2
+
/* These functions provide crypto support on the underlying transport */
BEGIN_EXTERN_C()
PHPAPI int php_stream_xport_crypto_setup(php_stream *stream, php_stream_xport_crypt_method_t crypto_method, php_stream *session_stream);
PHPAPI int php_stream_xport_crypto_enable(php_stream *stream, int activate);
+PHPAPI int php_stream_xport_crypto_get_status(php_stream *stream);
END_EXTERN_C()
typedef struct _php_stream_xport_crypto_param {
@@ -204,7 +210,8 @@ typedef struct _php_stream_xport_crypto_param {
} outputs;
enum {
STREAM_XPORT_CRYPTO_OP_SETUP,
- STREAM_XPORT_CRYPTO_OP_ENABLE
+ STREAM_XPORT_CRYPTO_OP_ENABLE,
+ STREAM_XPORT_CRYPTO_OP_GET_STATUS
} op;
} php_stream_xport_crypto_param;
diff --git a/main/streams/transports.c b/main/streams/transports.c
index 3231670dd9a..c01c5ada43c 100644
--- a/main/streams/transports.c
+++ b/main/streams/transports.c
@@ -397,6 +397,25 @@ PHPAPI int php_stream_xport_crypto_enable(php_stream *stream, int activate)
return ret;
}
+PHPAPI int php_stream_xport_crypto_get_status(php_stream *stream)
+{
+ php_stream_xport_crypto_param param;
+ int ret;
+
+ memset(¶m, 0, sizeof(param));
+ param.op = STREAM_XPORT_CRYPTO_OP_GET_STATUS;
+
+ ret = php_stream_set_option(stream, PHP_STREAM_OPTION_CRYPTO_API, 0, ¶m);
+
+ if (ret == PHP_STREAM_OPTION_RETURN_OK) {
+ return param.outputs.returncode;
+ }
+
+ php_error_docref("streams.crypto", E_WARNING, "This stream does not support SSL/crypto");
+
+ return STREAM_CRYPTO_STATUS_NONE;
+}
+
/* Similar to recv() system call; read data from the stream, optionally
* peeking, optionally retrieving OOB data */
PHPAPI int php_stream_xport_recvfrom(php_stream *stream, char *buf, size_t buflen,