Skip to content

Commit d822e94

Browse files
committed
Fix stream_socket_get_crypto_status() after supplemental read
Let's assume we have a stream with 5 queued bytes. When calling fread($s, 1), the underlying buffered stream will request a chunk of up to 8192 bytes immediately, but only return the requested 1 byte back to the reader. If the reader then requests fread($s, 10), _php_stream_read() will first return anything that was buffered but not yet read. If the requested length exceeds the number of buffered bytes (as is the case above), another read call is issued. This call will return nothing, because the stream only provides 4 more readable bytes, all of which are buffered. php_openssl_handle_ssl_error() (called by php_openssl_sockop_io()) will then incorrectly set last_status to WANT_READ, even though we've already read the remaining data. Furthermore, stream_select() can cause the same issue via php_openssl_sockop_cast(castas: PHP_STREAM_AS_FD_FOR_SELECT), which pre-fills the read buffer on SSL_pending() > 0. The subsequent fread() will lead to the same condition as above. Closes phpGH-22332
1 parent 51dd321 commit d822e94

3 files changed

Lines changed: 95 additions & 0 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.6.0alpha2
44

5+
- OpenSSL:
6+
. Fixed stream_socket_get_crypto_status() after supplemental read. (ilutov)
57

68
02 Jul 2026, PHP 8.6.0alpha1
79

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
--TEST--
2+
stream_socket_get_crypto_status(): reports status NONE after supplemental read
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_supplemental_read.pem.tmp';
12+
$peerName = 'crypto-status-supplemental-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+
fwrite($conn, "hello\n");
23+
24+
phpt_wait();
25+
fclose($conn);
26+
CODE;
27+
$serverCode = sprintf($serverCode, $certFile);
28+
29+
$clientCode = <<<'CODE'
30+
$ctx = stream_context_create(['ssl' => [
31+
'verify_peer' => false,
32+
'verify_peer_name' => false,
33+
'peer_name' => '%s',
34+
]]);
35+
36+
$client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx);
37+
stream_set_blocking($client, false);
38+
39+
$buf = '';
40+
$read = [$client];
41+
$write = $except = null;
42+
while (stream_select($read, $write, $except, 5)) {
43+
// Initially, read only the first char, then request more than is stored
44+
// in the buffer, triggering a supplemental read.
45+
$chunk = fread($client, strlen($buf) === 0 ? 1 : 10);
46+
if ($chunk === '' || $chunk === false) {
47+
/* A non-application record (e.g. a TLS 1.3 session ticket) may arrive first. */
48+
if (feof($client)) {
49+
break;
50+
}
51+
} else {
52+
$buf .= $chunk;
53+
if (strlen($buf) >= 6) {
54+
break;
55+
}
56+
}
57+
$read = [$client];
58+
$write = $except = null;
59+
}
60+
61+
echo trim($buf), "\n";
62+
/* A successful read clears the pending status back to NONE. */
63+
var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_NONE);
64+
65+
phpt_notify();
66+
fclose($client);
67+
CODE;
68+
$clientCode = sprintf($clientCode, $peerName);
69+
70+
include 'CertificateGenerator.inc';
71+
$certificateGenerator = new CertificateGenerator();
72+
$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile);
73+
74+
include 'ServerClientTestCase.inc';
75+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
76+
?>
77+
--CLEAN--
78+
<?php
79+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'crypto_status_supplemental_read.pem.tmp');
80+
?>
81+
--EXPECT--
82+
hello
83+
bool(true)

ext/openssl/xp_ssl.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,16 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si
30193019
php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), nr_bytes, 0);
30203020
}
30213021

3022+
/* This might be a supplemental read after consuming buffered data. If
3023+
* the read returned nothing, ignore status WANT_READ. */
3024+
if (read &&
3025+
supplemental &&
3026+
nr_bytes <= 0 &&
3027+
sslsock->last_status == STREAM_CRYPTO_STATUS_WANT_READ
3028+
) {
3029+
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
3030+
}
3031+
30223032
/* And if we were originally supposed to be blocking, let's reset the socket to that. */
30233033
if (began_blocked) {
30243034
php_openssl_set_blocking(sslsock, 1);

0 commit comments

Comments
 (0)