Skip to content

Commit 18fe337

Browse files
committed
Fix bug #51056: fread() on blocking stream will block even if data is available
This is applied only on socket connection which already returns immediately if there is no data in the buffer.
1 parent 1c1481b commit 18fe337

File tree

6 files changed

+94
-19
lines changed

6 files changed

+94
-19
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ PHP NEWS
3535
over socket binding for a cpu core. (David Carlier)
3636
. Added SKF_AD_QUEUE for cbpf filters. (David Carlier)
3737

38+
- Streams:
39+
. Fixed bug #51056: blocking fread() will block even if data is available.
40+
(Jakub Zelenka)
41+
3842
- Reflection:
3943
. Fix GH-9470 (ReflectionMethod constructor should not find private parent
4044
method). (ilutov)

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ PHP 8.3 UPGRADE NOTES
102102
other SAPIs, this directive is required when running as root because
103103
preloading is done before the SAPI switches to an unprivileged user.
104104

105+
- Streams:
106+
. Blocking fread() on socket connection returns immediately if there are
107+
any buffered data instead of waiting for more data.
108+
105109
========================================
106110
14. Performance Improvements
107111
========================================
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Bug #51056 (fread() on blocking stream will block even if data is available)
3+
--FILE--
4+
<?php
5+
6+
$serverCode = <<<'CODE'
7+
$server = stream_socket_server('tcp://127.0.0.1:64324');
8+
phpt_notify();
9+
10+
$conn = stream_socket_accept($server);
11+
12+
fwrite($conn, "Hello 1\n"); // 8 bytes
13+
usleep(50000);
14+
fwrite($conn, str_repeat('a', 300)."\n"); // 301 bytes
15+
usleep(50000);
16+
fwrite($conn, "Hello 1\n"); // 8 bytes
17+
18+
fclose($conn);
19+
fclose($server);
20+
CODE;
21+
22+
$clientCode = <<<'CODE'
23+
24+
phpt_wait();
25+
26+
$fp = fsockopen("tcp://127.0.0.1:64324");
27+
28+
while(!feof($fp)) {
29+
$data = fread($fp, 256);
30+
printf("fread read %d bytes\n", strlen($data));
31+
}
32+
CODE;
33+
34+
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
35+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
36+
37+
?>
38+
--EXPECT--
39+
fread read 8 bytes
40+
fread read 256 bytes
41+
fread read 45 bytes
42+
fread read 8 bytes
43+
fread read 0 bytes

main/php_streams.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ struct _php_stream {
225225
size_t readbuflen;
226226
zend_off_t readpos;
227227
zend_off_t writepos;
228+
ssize_t didread;
228229

229230
/* how much data to read when filling buffer */
230231
size_t chunk_size;

main/streams/streams.c

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,8 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
680680

681681
PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
682682
{
683-
ssize_t toread = 0, didread = 0;
683+
ssize_t toread = 0;
684+
stream->didread = 0;
684685

685686
while (size > 0) {
686687

@@ -699,7 +700,7 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
699700
stream->readpos += toread;
700701
size -= toread;
701702
buf += toread;
702-
didread += toread;
703+
stream->didread += toread;
703704
}
704705

705706
/* ignore eof here; the underlying state might have changed */
@@ -712,14 +713,14 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
712713
if (toread < 0) {
713714
/* Report an error if the read failed and we did not read any data
714715
* before that. Otherwise return the data we did read. */
715-
if (didread == 0) {
716+
if (stream->didread == 0) {
716717
return toread;
717718
}
718719
break;
719720
}
720721
} else {
721722
if (php_stream_fill_read_buffer(stream, size) != SUCCESS) {
722-
if (didread == 0) {
723+
if (stream->didread == 0) {
723724
return -1;
724725
}
725726
break;
@@ -736,7 +737,7 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
736737
}
737738
}
738739
if (toread > 0) {
739-
didread += toread;
740+
stream->didread += toread;
740741
buf += toread;
741742
size -= toread;
742743
} else {
@@ -752,11 +753,11 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
752753
}
753754
}
754755

755-
if (didread > 0) {
756-
stream->position += didread;
756+
if (stream->didread > 0) {
757+
stream->position += stream->didread;
757758
}
758759

759-
return didread;
760+
return stream->didread;
760761
}
761762

762763
/* Like php_stream_read(), but reading into a zend_string buffer. This has some similarity

main/streams/xp_socket.c

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,27 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun
120120
return didwrite;
121121
}
122122

123-
static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock)
123+
static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock, bool has_buffered_data)
124124
{
125125
int retval;
126-
struct timeval *ptimeout;
126+
struct timeval *ptimeout, zero_timeout;
127127

128128
if (!sock || sock->socket == -1) {
129129
return;
130130
}
131131

132132
sock->timeout_event = 0;
133133

134-
if (sock->timeout.tv_sec == -1)
134+
if (has_buffered_data) {
135+
/* If there is already buffered data, use no timeout. */
136+
zero_timeout.tv_sec = 0;
137+
zero_timeout.tv_usec = 0;
138+
ptimeout = &zero_timeout;
139+
} else if (sock->timeout.tv_sec == -1) {
135140
ptimeout = NULL;
136-
else
141+
} else {
137142
ptimeout = &sock->timeout;
143+
}
138144

139145
while(1) {
140146
retval = php_pollfd_for(sock->socket, PHP_POLLREADABLE, ptimeout);
@@ -153,21 +159,37 @@ static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data
153159
static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count)
154160
{
155161
php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract;
156-
ssize_t nr_bytes = 0;
157-
int err;
158162

159163
if (!sock || sock->socket == -1) {
160164
return -1;
161165
}
162166

167+
int recv_flags = 0;
168+
/* Special handling for blocking read. */
163169
if (sock->is_blocked) {
164-
php_sock_stream_wait_for_data(stream, sock);
165-
if (sock->timeout_event)
166-
return -1;
170+
/* Find out if there is any data buffered from the previous read. */
171+
bool has_buffered_data = stream->didread > 0;
172+
/* No need to wait if there is any data buffered or no timeout. */
173+
bool dont_wait = has_buffered_data ||
174+
(sock->timeout.tv_sec == 0 && sock->timeout.tv_usec == 0);
175+
/* Set MSG_DONTWAIT if no wait is needed or there is unlimited timeout which was
176+
* added by fix for #41984 commited in 9343c5404. */
177+
if (dont_wait || sock->timeout.tv_sec != -1) {
178+
recv_flags = MSG_DONTWAIT;
179+
}
180+
/* If the wait is needed or it is a platform without MSG_DONTWAIT support (e.g. Windows),
181+
* then poll for data. */
182+
if (!dont_wait || MSG_DONTWAIT == 0) {
183+
php_sock_stream_wait_for_data(stream, sock, has_buffered_data);
184+
if (sock->timeout_event) {
185+
/* It is ok to timeout if there is any data buffered so return 0, otherwise -1. */
186+
return has_buffered_data ? 0 : -1;
187+
}
188+
}
167189
}
168190

169-
nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && sock->timeout.tv_sec != -1) ? MSG_DONTWAIT : 0);
170-
err = php_socket_errno();
191+
ssize_t nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), recv_flags);
192+
int err = php_socket_errno();
171193

172194
if (nr_bytes < 0) {
173195
if (PHP_IS_TRANSIENT_ERROR(err)) {

0 commit comments

Comments
 (0)