diff --git a/NEWS b/NEWS index 6ba018dab89d..ec9efcbe02a3 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0alpha2 +- OpenSSL: + . Fixed stream_socket_get_crypto_status() after supplemental read. (ilutov) 02 Jul 2026, PHP 8.6.0alpha1 diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..83e933b01181 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4379,6 +4379,7 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) uint32_t fn_flags2 = op_array->fn_flags2; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; + const zend_property_info *prop_info = op_array->prop_info; *op_array = *orig_op_array; op_array->function_name = function_name; op_array->scope = scope; @@ -4386,6 +4387,7 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) op_array->fn_flags2 = fn_flags2; op_array->prototype = prototype; op_array->static_variables = ht; + op_array->prop_info = prop_info; } static void preload_fix_trait_methods(const zend_class_entry *ce) diff --git a/ext/opcache/tests/gh21770.phpt b/ext/opcache/tests/gh21770.phpt new file mode 100644 index 000000000000..d2fd52a4712b --- /dev/null +++ b/ext/opcache/tests/gh21770.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-21770 (Infinite recursion in property hook getter in opcache preloaded trait) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_gh21770.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +a, "\n"; + +$c = new C(); +$c->x = 42; +var_dump($c->x); +?> +--EXPECT-- +a +int(42) diff --git a/ext/opcache/tests/preload_gh21770.inc b/ext/opcache/tests/preload_gh21770.inc new file mode 100644 index 000000000000..7433a15eca92 --- /dev/null +++ b/ext/opcache/tests/preload_gh21770.inc @@ -0,0 +1,20 @@ + $this->a; + } +} + +trait X { + public int $x = 0 { + set(int $value) => $value; + } +} + +class B { + use A; +} + +class C { + use X; +} diff --git a/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt b/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt new file mode 100644 index 000000000000..9044b1bbdbe4 --- /dev/null +++ b/ext/openssl/tests/stream_socket_get_crypto_status_supplemental_read.phpt @@ -0,0 +1,83 @@ +--TEST-- +stream_socket_get_crypto_status(): reports status NONE after supplemental read +--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); + + 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); + + $buf = ''; + $read = [$client]; + $write = $except = null; + while (stream_select($read, $write, $except, 5)) { + // Initially, read only the first char, then request more than is stored + // in the buffer, triggering a supplemental read. + $chunk = fread($client, strlen($buf) === 0 ? 1 : 10); + 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-- +hello +bool(true) diff --git a/ext/openssl/tests/stream_supplemental_read_timeout.phpt b/ext/openssl/tests/stream_supplemental_read_timeout.phpt new file mode 100644 index 000000000000..0e36f146bf9e --- /dev/null +++ b/ext/openssl/tests/stream_supplemental_read_timeout.phpt @@ -0,0 +1,90 @@ +--TEST-- +Timeout for supplemental read at end of a blocking stream in SSL stream wrapper +--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); + + 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, true); + stream_set_timeout($client, 5); + $start = hrtime(true); + + $buf = ''; + $read = [$client]; + $write = $except = null; + while (true) { + if (!stream_select($read, $write, $except, 5)) { + break; + } + + // Initially, read only the first char, then request more than is stored + // in the buffer, triggering a supplemental read. + $chunk = fread($client, strlen($buf) === 0 ? 1 : 10); + 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"; + + $diff = (hrtime(true) - $start) / 1e9; + var_dump($diff < 4.0); + + 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-- +hello +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index bc483df617b1..93a9971b4a78 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2874,7 +2874,11 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si /* Only do this if SSL is active. */ if (sslsock->ssl_active) { - int retry = 1; + /* We have already returned some buffered data. Don't retry and don't + * block. We're just trying to fill the buffer more, but the stream might + * be empty, so we don't want to wait in vain. */ + bool supplemental = stream->has_buffered_data; + int retry = !supplemental; struct timeval start_time; struct timeval *timeout = NULL; bool began_blocked = sslsock->s.is_blocked; @@ -2887,11 +2891,11 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } /* never use a timeout with non-blocking sockets */ - if (began_blocked) { + if (began_blocked && !supplemental) { timeout = &sslsock->s.timeout; } - if (timeout) { + if (timeout || supplemental) { php_openssl_set_blocking(sslsock, 0); } @@ -2966,7 +2970,7 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } /* Don't loop indefinitely in non-blocking mode if no data is available */ - if (began_blocked == 0) { + if (began_blocked == 0 || supplemental) { break; } @@ -3015,6 +3019,16 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), nr_bytes, 0); } + /* This might be a supplemental read after consuming buffered data. If + * the read returned nothing, ignore status WANT_READ. */ + if (read && + supplemental && + nr_bytes <= 0 && + sslsock->last_status == STREAM_CRYPTO_STATUS_WANT_READ + ) { + sslsock->last_status = STREAM_CRYPTO_STATUS_NONE; + } + /* And if we were originally supposed to be blocking, let's reset the socket to that. */ if (began_blocked) { php_openssl_set_blocking(sslsock, 1);