Skip to content

Fix TLS stream EOF detection after close_notify with stale errno - #24132

Merged
bukka merged 1 commit into
php:PHP-8.4from
bukka:openssl_tls_eof_stale_eagain_84
Oct 5, 2026
Merged

bukka merged 1 commit into
php:PHP-8.4from
bukka:openssl_tls_eof_stale_eagain_84

Conversation

@bukka

@bukka bukka commented Oct 5, 2026

Copy link
Copy Markdown
Member

php_openssl_handle_ssl_error() sets errno to EAGAIN on SSL_ERROR_WANT_READ and SSL_ERROR_WANT_WRITE, and php_openssl_sockop_io() reads it back to avoid marking a non-blocking read that needs to wait as EOF. OpenSSL resets errno before every recv() on POSIX systems, but on Windows it uses the Winsock error state instead and never touches errno, so the EAGAIN stays set across every later successful read. A close_notify received afterwards returned SSL_ERROR_ZERO_RETURN but did not set stream->eof, and feof() stayed false while the TCP connection was still open.

Decide EOF from the SSL error code, which already says whether the operation just needs to wait, instead of from errno.

php_openssl_handle_ssl_error() sets errno to EAGAIN on SSL_ERROR_WANT_READ
and SSL_ERROR_WANT_WRITE, and php_openssl_sockop_io() reads it back to avoid
marking a non-blocking read that needs to wait as EOF. OpenSSL resets errno
before every recv() on POSIX systems, but on Windows it uses the Winsock
error state instead and never touches errno, so the EAGAIN stays set across
every later successful read. A close_notify received afterwards returned
SSL_ERROR_ZERO_RETURN but did not set stream->eof, and feof() stayed false
while the TCP connection was still open.

Decide EOF from the SSL error code, which already says whether the
operation just needs to wait, instead of from errno.
bukka referenced this pull request Oct 5, 2026
StreamPollHandle took the descriptor through the select cast. On a TLS
stream that cast reads: it moves what OpenSSL holds decrypted into the
stream buffer so that stream_select() can report it. Adding a watcher to
an Io\Poll\Context therefore read from the stream, filled the buffer of a
stream set unbuffered with stream_set_read_buffer() and changed
unread_bytes. That is not how the poll API should behave. Adding a handle
is a registration and must leave the stream as it found it, and the data
that moved was not reported by the watcher anyway, as bytes held by the
stream layer are not readiness of the descriptor.

A new cast PHP_STREAM_AS_FD_FOR_POLL returns the descriptor and nothing
else. The socket, TLS, plain and pgsql wrappers answer it, a userspace
wrapper sees it as STREAM_CAST_FOR_SELECT and the stream it returns is
cast the same way, and a filtered stream allows it like the select cast.
StreamPollHandle uses it, so adding a TLS stream leaves its buffer and
unread_bytes as they were. The select cast and stream_select() are
unchanged.

This is fixed in 8.6 because the API is new there and this is the
behaviour it should ship with. Keeping a read inside the registration
would complicate things later: once reads on a TLS stream can suspend,
the cast would become a nested read on a stream with an operation in
flight, and changing the cast then would change the visible behaviour of
a released API.
@bukka
bukka merged commit 614c628 into php:PHP-8.4 Oct 5, 2026
18 checks passed
bukka added a commit that referenced this pull request Oct 5, 2026
* PHP-8.4:
  Fix TLS stream EOF detection after close_notify with stale errno (#24132)
bukka added a commit that referenced this pull request Oct 5, 2026
* PHP-8.5:
  Fix TLS stream EOF detection after close_notify with stale errno (#24132)
bukka added a commit that referenced this pull request Oct 5, 2026
* PHP-8.6:
  Fix TLS stream EOF detection after close_notify with stale errno (#24132)
arnaud-lb added a commit to nicolas-grekas/php-src that referenced this pull request Oct 5, 2026
* up/PHP-8.4:
  [ci skip] NEWS
  Rethrow exceptions from destructors called by the GC in a fiber (php#24118)
  ext/intl: Use byte offsets in IntlDateFormatter parsing
  ext/dom: Restore the XPath context after a reentrant evaluation
  Fix TLS stream EOF detection after close_notify with stale errno (php#24132)
  Re-generate outdated parse_date.c file
  ext/standard: Close owned proc_open descriptors on setup failure
  ext/standard: Keep IPTC headers local to each call
  ext/standard: Reject incomplete sha1_file reads
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant