Skip to content

Commit 90a9913

Browse files
committed
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.
1 parent 631bf8a commit 90a9913

14 files changed

Lines changed: 309 additions & 7 deletions

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ PHP NEWS
113113
and Openssl\Session class. (Jakub Zelenka)
114114
. Added TLS external PSK support for streams with new context options and
115115
Openssl\Psk class. (Jakub Zelenka)
116+
. Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE.
117+
(Jakub Zelenka)
116118

117119
- PCNTL:
118120
. pcntl_exec() now throws a ValueError if the $args array is not a list

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ PHP 8.6 UPGRADE NOTES
323323
RFC: https://wiki.php.net/rfc/clamp_v2
324324
. `stream_last_errors()` and `stream_clear_errors()`.
325325
RFC: https://wiki.php.net/rfc/stream_errors
326+
. stream_socket_get_crypto_status()
326327

327328
- Zip:
328329
. Added ZipArchive::openString() method.
@@ -389,6 +390,9 @@ PHP 8.6 UPGRADE NOTES
389390

390391
- Standard
391392
. ARRAY_FILTER_USE_VALUE.
393+
. STREAM_CRYPTO_STATUS_NONE
394+
. STREAM_CRYPTO_STATUS_WANT_READ
395+
. STREAM_CRYPTO_STATUS_WANT_WRITE
392396

393397
========================================
394398
11. Changes to INI File Handling
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
stream_socket_get_crypto_status(): constants and behavior on a non-crypto stream
3+
--EXTENSIONS--
4+
openssl
5+
--FILE--
6+
<?php
7+
/* The status constants. */
8+
var_dump(STREAM_CRYPTO_STATUS_NONE);
9+
var_dump(STREAM_CRYPTO_STATUS_WANT_READ);
10+
var_dump(STREAM_CRYPTO_STATUS_WANT_WRITE);
11+
12+
/* A plain (non-SSL) socket has no pending crypto operation, so the status
13+
* is STREAM_CRYPTO_STATUS_NONE. */
14+
$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
15+
var_dump(@stream_socket_get_crypto_status($server) === STREAM_CRYPTO_STATUS_NONE);
16+
fclose($server);
17+
?>
18+
--EXPECT--
19+
int(0)
20+
int(1)
21+
int(2)
22+
bool(true)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
--TEST--
2+
stream_socket_get_crypto_status(): reports WANT_READ/WANT_WRITE during a non-blocking handshake
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
?>
9+
--FILE--
10+
<?php
11+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_handshake.pem.tmp';
12+
$peerName = 'crypto-status-handshake';
13+
14+
/* Plain blocking TLS server. */
15+
$serverCode = <<<'CODE'
16+
$ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
17+
$flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
18+
$server = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, $flags, $ctx);
19+
phpt_notify_server_start($server);
20+
21+
$conn = stream_socket_accept($server, 30);
22+
if ($conn) {
23+
fwrite($conn, "ok\n");
24+
phpt_wait();
25+
fclose($conn);
26+
}
27+
CODE;
28+
$serverCode = sprintf($serverCode, $certFile);
29+
30+
/* Client connects over plain TCP, then completes the TLS handshake in non-blocking mode, using
31+
* the reported crypto status to select the right direction to wait on. */
32+
$clientCode = <<<'CODE'
33+
$ctx = stream_context_create(['ssl' => [
34+
'verify_peer' => false,
35+
'verify_peer_name' => false,
36+
'peer_name' => '%s',
37+
]]);
38+
39+
$client = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx);
40+
stream_set_blocking($client, false);
41+
42+
$sawWant = false;
43+
$pendingAlwaysWant = true;
44+
45+
do {
46+
$r = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
47+
if ($r === 0) {
48+
$status = stream_socket_get_crypto_status($client);
49+
if ($status === STREAM_CRYPTO_STATUS_WANT_READ
50+
|| $status === STREAM_CRYPTO_STATUS_WANT_WRITE) {
51+
$sawWant = true;
52+
} else {
53+
/* must never be NONE while the handshake is still pending */
54+
$pendingAlwaysWant = false;
55+
}
56+
57+
/* Wait on the direction the engine actually asked for. */
58+
$read = $write = $except = null;
59+
if ($status === STREAM_CRYPTO_STATUS_WANT_WRITE) {
60+
$write = [$client];
61+
} else {
62+
$read = [$client];
63+
}
64+
stream_select($read, $write, $except, 1);
65+
}
66+
} while ($r === 0);
67+
68+
var_dump($r);
69+
var_dump($sawWant);
70+
var_dump($pendingAlwaysWant);
71+
/* After a completed handshake the status is reset to NONE. */
72+
var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_NONE);
73+
74+
stream_set_blocking($client, true);
75+
echo trim(fgets($client)), "\n";
76+
phpt_notify();
77+
fclose($client);
78+
CODE;
79+
$clientCode = sprintf($clientCode, $peerName);
80+
81+
include 'CertificateGenerator.inc';
82+
$certificateGenerator = new CertificateGenerator();
83+
$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile);
84+
85+
include 'ServerClientTestCase.inc';
86+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
87+
?>
88+
--CLEAN--
89+
<?php
90+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_handshake.pem.tmp');
91+
?>
92+
--EXPECT--
93+
bool(true)
94+
bool(true)
95+
bool(true)
96+
bool(true)
97+
ok
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
--TEST--
2+
stream_socket_get_crypto_status(): reports WANT_READ on a non-blocking read with no application data
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
?>
9+
--FILE--
10+
<?php
11+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_read.pem.tmp';
12+
$peerName = 'crypto-status-read';
13+
14+
$serverCode = <<<'CODE'
15+
$ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
16+
$flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
17+
$server = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, $flags, $ctx);
18+
phpt_notify_server_start($server);
19+
20+
$conn = stream_socket_accept($server, 30);
21+
22+
/* Do not send anything until the client has performed its first read, so that the read is
23+
* guaranteed to find no application data. */
24+
phpt_wait();
25+
fwrite($conn, "hello\n");
26+
27+
phpt_wait();
28+
fclose($conn);
29+
CODE;
30+
$serverCode = sprintf($serverCode, $certFile);
31+
32+
$clientCode = <<<'CODE'
33+
$ctx = stream_context_create(['ssl' => [
34+
'verify_peer' => false,
35+
'verify_peer_name' => false,
36+
'peer_name' => '%s',
37+
]]);
38+
39+
$client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx);
40+
stream_set_blocking($client, false);
41+
42+
/* No application data has been sent yet - a non-blocking read returns nothing and the crypto
43+
* status reflects that the OpenSSL wants to read. */
44+
$data = fread($client, 100);
45+
var_dump($data === '' || $data === false);
46+
var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_WANT_READ);
47+
48+
/* Now let the server send. */
49+
phpt_notify();
50+
51+
$buf = '';
52+
$read = [$client];
53+
$write = $except = null;
54+
while (stream_select($read, $write, $except, 5)) {
55+
$chunk = fread($client, 100);
56+
if ($chunk === '' || $chunk === false) {
57+
/* A non-application record (e.g. a TLS 1.3 session ticket) may arrive first. */
58+
if (feof($client)) {
59+
break;
60+
}
61+
} else {
62+
$buf .= $chunk;
63+
if (strlen($buf) >= 6) {
64+
break;
65+
}
66+
}
67+
$read = [$client];
68+
$write = $except = null;
69+
}
70+
71+
echo trim($buf), "\n";
72+
/* A successful read clears the pending status back to NONE. */
73+
var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_NONE);
74+
75+
phpt_notify();
76+
fclose($client);
77+
CODE;
78+
$clientCode = sprintf($clientCode, $peerName);
79+
80+
include 'CertificateGenerator.inc';
81+
$certificateGenerator = new CertificateGenerator();
82+
$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile);
83+
84+
include 'ServerClientTestCase.inc';
85+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
86+
?>
87+
--CLEAN--
88+
<?php
89+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_read.pem.tmp');
90+
?>
91+
--EXPECT--
92+
bool(true)
93+
bool(true)
94+
hello
95+
bool(true)

ext/openssl/xp_ssl.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ typedef struct _php_openssl_netstream_data_t {
207207
int enable_on_connect;
208208
int is_client;
209209
int ssl_active;
210+
int last_status;
210211
php_stream_xport_crypt_method_t method;
211212
php_openssl_handshake_bucket_t *reneg;
212213
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
271272
* packets: retry in next iteration */
272273
errno = EAGAIN;
273274
retry = is_init ? true : sslsock->s.is_blocked;
275+
if (!retry) {
276+
sslsock->last_status = err == SSL_ERROR_WANT_READ ?
277+
STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE;
278+
}
274279
break;
275280
case SSL_ERROR_SYSCALL:
276281
if (ERR_peek_error() == 0) {
@@ -2701,6 +2706,8 @@ static int php_openssl_enable_crypto(php_stream *stream,
27012706
int cert_captured = 0;
27022707
X509 *peer_cert;
27032708

2709+
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
2710+
27042711
if (cparam->inputs.activate && !sslsock->ssl_active) {
27052712
struct timeval start_time, *timeout;
27062713
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
29002907

29012908
/* Now, do the IO operation. Don't block if we can't complete... */
29022909
ERR_clear_error();
2910+
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
29032911
if (read) {
29042912
nr_bytes = SSL_read(sslsock->ssl_handle, buf, (int)count);
29052913

@@ -2974,6 +2982,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si
29742982
php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
29752983
(POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL);
29762984
}
2985+
} else if (err == SSL_ERROR_WANT_READ) {
2986+
sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ;
2987+
} else if (err == SSL_ERROR_WANT_WRITE) {
2988+
sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE;
29772989
}
29782990
}
29792991

@@ -3445,6 +3457,9 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
34453457
case STREAM_XPORT_CRYPTO_OP_ENABLE:
34463458
cparam->outputs.returncode = php_openssl_enable_crypto(stream, sslsock, cparam);
34473459
return PHP_STREAM_OPTION_RETURN_OK;
3460+
case STREAM_XPORT_CRYPTO_OP_GET_STATUS:
3461+
cparam->outputs.returncode = sslsock->last_status;
3462+
return PHP_STREAM_OPTION_RETURN_OK;
34483463
default:
34493464
/* fall through */
34503465
break;

ext/standard/basic_functions.stub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3484,6 +3484,11 @@ function stream_socket_sendto($socket, string $data, int $flags = 0, string $add
34843484
*/
34853485
function stream_socket_enable_crypto($stream, bool $enable, ?int $crypto_method = null, $session_stream = null): int|bool {}
34863486

3487+
/**
3488+
* @param resource $stream
3489+
*/
3490+
function stream_socket_get_crypto_status($stream): int {}
3491+
34873492
#ifdef HAVE_SHUTDOWN
34883493
/** @param resource $stream */
34893494
function stream_socket_shutdown($stream, int $mode): bool {}

ext/standard/basic_functions_arginfo.h

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/standard/basic_functions_decl.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/standard/file.stub.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,23 @@
256256
*/
257257
const STREAM_CRYPTO_PROTO_TLSv1_3 = UNKNOWN;
258258

259+
/**
260+
* @var int
261+
* @cvalue STREAM_CRYPTO_STATUS_NONE
262+
*/
263+
const STREAM_CRYPTO_STATUS_NONE = UNKNOWN;
264+
/**
265+
* @var int
266+
* @cvalue STREAM_CRYPTO_STATUS_WANT_READ
267+
*/
268+
const STREAM_CRYPTO_STATUS_WANT_READ = UNKNOWN;
269+
/**
270+
* @var int
271+
* @cvalue STREAM_CRYPTO_STATUS_WANT_WRITE
272+
*/
273+
const STREAM_CRYPTO_STATUS_WANT_WRITE = UNKNOWN;
274+
275+
259276
/**
260277
* @var int
261278
* @cvalue STREAM_SHUT_RD

0 commit comments

Comments
 (0)