From 69cf50c597166a750dee4fbc881e5a28cef88753 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 26 Oct 2025 15:45:09 +0100 Subject: [PATCH] Add stream crypto status for exposing OSSL WANT_READ / WANT_WRITE 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. --- ...stream_socket_get_crypto_status_basic.phpt | 22 +++++ ...am_socket_get_crypto_status_handshake.phpt | 97 +++++++++++++++++++ .../stream_socket_get_crypto_status_read.phpt | 95 ++++++++++++++++++ ext/openssl/xp_ssl.c | 15 +++ ext/standard/basic_functions.stub.php | 5 + ext/standard/basic_functions_arginfo.h | 6 +- ext/standard/basic_functions_decl.h | 8 +- ext/standard/file.stub.php | 17 ++++ ext/standard/file_arginfo.h | 5 +- ext/standard/streamsfuncs.c | 12 +++ main/streams/php_stream_transport.h | 9 +- main/streams/transports.c | 19 ++++ 12 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 ext/openssl/tests/stream_socket_get_crypto_status_basic.phpt create mode 100644 ext/openssl/tests/stream_socket_get_crypto_status_handshake.phpt create mode 100644 ext/openssl/tests/stream_socket_get_crypto_status_read.phpt 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 000000000000..b659eee79e78 --- /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-- + +--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 000000000000..2a1c554a79d0 --- /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-- + +--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); + 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-- + +--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 000000000000..308950a8ef03 --- /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-- + +--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); + + /* 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-- + +--EXPECT-- +bool(true) +bool(true) +hello +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 307cc3489c3c..6f91904aa33c 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) { @@ -2684,6 +2689,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; @@ -2883,6 +2890,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); @@ -2957,6 +2965,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; } } @@ -3417,6 +3429,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 b63d4f404a7a..c88fabb956f9 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 ca72962d34e6..a8a26821f306 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: dcb08d7065d61ed6913f29e5d64e78d1fb4fce77 + * Stub hash: 8e3659c0fb52c10e893782c6c43fbf1fa309453d * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -1926,6 +1926,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_stream_socket_enable_crypto, 0, ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, session_stream, "null") ZEND_END_ARG_INFO() +#define arginfo_stream_socket_get_crypto_status arginfo_fpassthru + #if defined(HAVE_SHUTDOWN) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_socket_shutdown, 0, 2, _IS_BOOL, 0) ZEND_ARG_INFO(0, stream) @@ -2823,6 +2825,7 @@ ZEND_FUNCTION(stream_socket_get_name); ZEND_FUNCTION(stream_socket_recvfrom); ZEND_FUNCTION(stream_socket_sendto); ZEND_FUNCTION(stream_socket_enable_crypto); +ZEND_FUNCTION(stream_socket_get_crypto_status); #if defined(HAVE_SHUTDOWN) ZEND_FUNCTION(stream_socket_shutdown); #endif @@ -3438,6 +3441,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(stream_socket_recvfrom, arginfo_stream_socket_recvfrom) ZEND_FE(stream_socket_sendto, arginfo_stream_socket_sendto) ZEND_FE(stream_socket_enable_crypto, arginfo_stream_socket_enable_crypto) + ZEND_FE(stream_socket_get_crypto_status, arginfo_stream_socket_get_crypto_status) #if defined(HAVE_SHUTDOWN) ZEND_FE(stream_socket_shutdown, arginfo_stream_socket_shutdown) #endif diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 8e92337ba378..8d8dfdec9d5f 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: dcb08d7065d61ed6913f29e5d64e78d1fb4fce77 */ + * Stub hash: 8e3659c0fb52c10e893782c6c43fbf1fa309453d */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_dcb08d7065d61ed6913f29e5d64e78d1fb4fce77_H -#define ZEND_BASIC_FUNCTIONS_DECL_dcb08d7065d61ed6913f29e5d64e78d1fb4fce77_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_8e3659c0fb52c10e893782c6c43fbf1fa309453d_H +#define ZEND_BASIC_FUNCTIONS_DECL_8e3659c0fb52c10e893782c6c43fbf1fa309453d_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_dcb08d7065d61ed6913f29e5d64e78d1fb4fce77_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_8e3659c0fb52c10e893782c6c43fbf1fa309453d_H */ diff --git a/ext/standard/file.stub.php b/ext/standard/file.stub.php index 5e12c43f397c..d7b1fef17cdc 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 b3888925ee9a..24e3722cd86e 100644 --- a/ext/standard/file_arginfo.h +++ b/ext/standard/file_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit file.stub.php instead. - * Stub hash: c394e14cd32587ce9ad0503e21c6c4cf5b301697 */ + * Stub hash: 0c62c6fb217a87010a9e2e63d4b104cde0138655 */ static void register_file_symbols(int module_number) { @@ -52,6 +52,9 @@ static void register_file_symbols(int module_number) REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_TLSv1_1", STREAM_CRYPTO_METHOD_TLSv1_1_SERVER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_TLSv1_2", STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_TLSv1_3", STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("STREAM_CRYPTO_STATUS_NONE", STREAM_CRYPTO_STATUS_NONE, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("STREAM_CRYPTO_STATUS_WANT_READ", STREAM_CRYPTO_STATUS_WANT_READ, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("STREAM_CRYPTO_STATUS_WANT_WRITE", STREAM_CRYPTO_STATUS_WANT_WRITE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_SHUT_RD", STREAM_SHUT_RD, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_SHUT_WR", STREAM_SHUT_WR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_SHUT_RDWR", STREAM_SHUT_RDWR, CONST_PERSISTENT); diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 3609ee1f3f24..bad645f8668a 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 0125035aaa69..60bea8e9e1fc 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 3231670dd9a3..c01c5ada43cb 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,