Bug #72333 [NEW]: fwrite() on non-blocking SSL sockets doesn't work after SSL_ERROR_WANT_...

From: Date: Sat, 04 Jun 2016 09:45:28 +0000
Subject: Bug #72333 [NEW]: fwrite() on non-blocking SSL sockets doesn't work after SSL_ERROR_WANT_...
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-201444@lists.php.net to get a copy of this message
From:             webmaster_20160604 at cubiclesoft dot com
Operating system: All
PHP version:      5.6.22
Package:          Streams related
Bug Type:         Bug
Bug description:fwrite() on non-blocking SSL sockets doesn't work after SSL_ERROR_WANT_...

Description:
------------
The userland test script demonstrates the problem.  A large fwrite()
call ends up returning a $result smaller than strlen($str) when
non-blocking sockets are used.  For regular TCP sockets, this isn't a
huge deal - just lop off the bytes sent and try again later with the
remaining data when stream_select() returns.  However, that becomes a
serious problem when a SSL-enabled non-blocking socket is used to
attempt to send a large amount of data.

Behind the scenes in the stream handler function
php_openssl_sockop_write() (ext\openssl\xp_ssl.c),
SSL_write()/SSL_get_error() returns an error code of SSL_ERROR_WANT_READ
or SSL_ERROR_WANT_WRITE when the first fwrite() function is called. 
When the socket is non-blocking, the number of retries returned is 0 and
therefore the function immediately returns.  Unfortunately, this has the
unintended effect of breaking the underlying stream even though I don't
precisely know how that happens - from the userland perspective, $str
never changes so how the pointers change is a bit of a mystery.  I
digress.

Ultimately, the real culprit is SSL_write().  By default, it wants the
same EXACT buffer (i.e. pointer) and buffer length when it is called
again.  When blocking sockets are used, the php_openssl_sockop_write()
function simply retries the SSL_write() call continuously until it
succeeds and therefore there is no issue.  However, when non-blocking
sockets are used, the function returns to the caller but effectively
destroys the socket since there is no apparent way to pass the same
buffer to SSL_write() ever again.  Attempting to call fwrite() with the
same input as before results in a return value of 0 and this PHP warning
is emitted:

PHP Warning:  fwrite(): SSL operation failed with code 1. OpenSSL Error
messages:
error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in [FILE]
on line [LINE].

Even though the value of $str never changes from the userland
perspective in the example, the 'const char *buf' pointer is effectively
different by the time it reaches the second SSL_write() call, which
subsequently fails when it detects the difference, which is typically
where the 'bad write retry' error comes from.

One possible solution could be to call:

SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE |
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);

Or:

SSL_set_mode(ssl, SSL_MODE_ENABLE_PARTIAL_WRITE |
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);

During setup of the SSL context or later after the SSL object is
initialized.  Even if $str relocates elsewhere in memory, it won't be a
problem with the moving write buffer option and the partial write option
allows for the data that was sent to be dropped just like non-SSL
non-blocking sockets.  But you might want to read the manpages on those
options as the authors have written a few "caveats".

To avoid conflicts with the current code that works fine for blocking
SSL sockets, those two options should probably only be set when
non-blocking SSL sockets are in use and adjusted when the blocking mode
changes to reduce the potential for severe breakage with blocking SSL
sockets.

It would also be extremely helpful to know which error type (i.e.
WANT_READ/WANT_WRITE) was returned from SSL_write()/SSL_get_error() on
failure so the socket can be passed into the correct stream_select()
array.  It's a related bug in that the details about the error aren't
available to userland (EAGAIN isn't sufficient to determine read vs.
write) and therefore fwrite() subsequently reduces the performance of
stream_select() for non-blocking SSL sockets (i.e. not knowing wastes
CPU cycles in a busy loop).

Test script:
---------------
<?php
	$context = stream_context_create();
	$fp = stream_socket_client("tls://www.google.com:443", $errornum,
$errorstr, 300, STREAM_CLIENT_CONNECT, $context);
var_dump($fp);
var_dump($errornum);
var_dump($errorstr);

	stream_set_blocking($fp, 0);

	$str = "GET / HTTP/1.1\r\n";
	$str .= str_repeat("a", 1048576);

	$result = fwrite($fp, $str);
var_dump($result);

	$result = fwrite($fp, $str);
var_dump($result);
?>

Expected result:
----------------
The first fwrite() to not break the underlying stream.

The second fwrite() to not fail with a PHP Warning.

Actual result:
--------------
The first fwrite() breaks the underlying stream due to the loss of a
valid pointer to pass to SSL_write() later to resume operations.

The second fwrite() subsequently fails with the PHP Warning:

PHP Warning:  fwrite(): SSL operation failed with code 1. OpenSSL Error
messages:
error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in [FILE]
on line [LINE].


-- 
Edit bug report at https://bugs.php.net/bug.php?id=72333&edit=1
-- 
Try a snapshot (PHP 5.4):   https://bugs.php.net/fix.php?id=72333&r=trysnapshot54
Try a snapshot (PHP 5.5):   https://bugs.php.net/fix.php?id=72333&r=trysnapshot55
Try a snapshot (trunk):     https://bugs.php.net/fix.php?id=72333&r=trysnapshottrunk
Fixed in SVN:               https://bugs.php.net/fix.php?id=72333&r=fixed
Fixed in release:           https://bugs.php.net/fix.php?id=72333&r=alreadyfixed
Need backtrace:             https://bugs.php.net/fix.php?id=72333&r=needtrace
Need Reproduce Script:      https://bugs.php.net/fix.php?id=72333&r=needscript
Try newer version:          https://bugs.php.net/fix.php?id=72333&r=oldversion
Not developer issue:        https://bugs.php.net/fix.php?id=72333&r=support
Expected behavior:          https://bugs.php.net/fix.php?id=72333&r=notwrong
Not enough info:            https://bugs.php.net/fix.php?id=72333&r=notenoughinfo
Submitted twice:            https://bugs.php.net/fix.php?id=72333&r=submittedtwice
register_globals:           https://bugs.php.net/fix.php?id=72333&r=globals
PHP 4 support discontinued: https://bugs.php.net/fix.php?id=72333&r=php4
Daylight Savings:           https://bugs.php.net/fix.php?id=72333&r=dst
IIS Stability:              https://bugs.php.net/fix.php?id=72333&r=isapi
Install GNU Sed:            https://bugs.php.net/fix.php?id=72333&r=gnused
Floating point limitations: https://bugs.php.net/fix.php?id=72333&r=float
No Zend Extensions:         https://bugs.php.net/fix.php?id=72333&r=nozend
MySQL Configuration Error:  https://bugs.php.net/fix.php?id=72333&r=mysqlcfg



Thread (5 messages)

« previous php.bugs (#201444) next »