Skip to content

Commit 69cf50c

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 7092ff5 commit 69cf50c

12 files changed

Lines changed: 303 additions & 7 deletions
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) {
@@ -2684,6 +2689,8 @@ static int php_openssl_enable_crypto(php_stream *stream,
26842689
int cert_captured = 0;
26852690
X509 *peer_cert;
26862691

2692+
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
2693+
26872694
if (cparam->inputs.activate && !sslsock->ssl_active) {
26882695
struct timeval start_time, *timeout;
26892696
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
28832890

28842891
/* Now, do the IO operation. Don't block if we can't complete... */
28852892
ERR_clear_error();
2893+
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
28862894
if (read) {
28872895
nr_bytes = SSL_read(sslsock->ssl_handle, buf, (int)count);
28882896

@@ -2957,6 +2965,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si
29572965
php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
29582966
(POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL);
29592967
}
2968+
} else if (err == SSL_ERROR_WANT_READ) {
2969+
sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ;
2970+
} else if (err == SSL_ERROR_WANT_WRITE) {
2971+
sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE;
29602972
}
29612973
}
29622974

@@ -3417,6 +3429,9 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
34173429
case STREAM_XPORT_CRYPTO_OP_ENABLE:
34183430
cparam->outputs.returncode = php_openssl_enable_crypto(stream, sslsock, cparam);
34193431
return PHP_STREAM_OPTION_RETURN_OK;
3432+
case STREAM_XPORT_CRYPTO_OP_GET_STATUS:
3433+
cparam->outputs.returncode = sslsock->last_status;
3434+
return PHP_STREAM_OPTION_RETURN_OK;
34203435
default:
34213436
/* fall through */
34223437
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

ext/standard/file_arginfo.h

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

ext/standard/streamsfuncs.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,18 @@ PHP_FUNCTION(stream_socket_enable_crypto)
16381638
}
16391639
/* }}} */
16401640

1641+
/* Get crypto status */
1642+
PHP_FUNCTION(stream_socket_get_crypto_status)
1643+
{
1644+
php_stream *stream;
1645+
1646+
ZEND_PARSE_PARAMETERS_START(1, 1)
1647+
PHP_Z_PARAM_STREAM(stream)
1648+
ZEND_PARSE_PARAMETERS_END();
1649+
1650+
RETURN_LONG(php_stream_xport_crypto_get_status(stream));
1651+
}
1652+
16411653
/* {{{ Determine what file will be opened by calls to fopen() with a relative path */
16421654
PHP_FUNCTION(stream_resolve_include_path)
16431655
{

0 commit comments

Comments
 (0)