Skip to content

Commit 3f4e7b8

Browse files
committed
Disallow concurrent access to stream via signal handlers
1 parent b658fc9 commit 3f4e7b8

5 files changed

Lines changed: 243 additions & 2 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
Stream cannot be accessed concurrently via signal handlers
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--SKIPIF--
7+
<?php
8+
$pair = @stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
9+
if (!$pair) die("skip stream_socket_pair() not available");
10+
if (!function_exists('pcntl_fork')) die("skip pcntl_fork() not available");
11+
?>
12+
--FILE--
13+
<?php
14+
15+
[$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
16+
stream_set_blocking($read, true);
17+
stream_set_timeout($read, 3600);
18+
19+
pcntl_signal(SIGUSR1, function (int $signo) use ($read): void {
20+
echo "handler\n";
21+
try {
22+
fread($read, 1);
23+
} catch (Error $e) {
24+
echo $e::class, ": ", $e->getMessage(), "\n";
25+
}
26+
try {
27+
fclose($read);
28+
} catch (Error $e) {
29+
echo $e::class, ": ", $e->getMessage(), "\n";
30+
}
31+
}, restart_syscalls: true);
32+
33+
$pid = pcntl_fork();
34+
if ($pid === 0) {
35+
fwrite($write, "1");
36+
fclose($read);
37+
usleep(200_000); // let parent block in fread()
38+
posix_kill(posix_getppid(), SIGUSR1);
39+
usleep(200_000); // let parent block in fread()
40+
fwrite($write, "hello\n");
41+
fclose($write);
42+
exit(0);
43+
}
44+
45+
// Wait for child to start
46+
fread($read, 1);
47+
48+
$result = fread($read, 1024);
49+
var_dump($result);
50+
51+
pcntl_waitpid($pid, $status);
52+
fclose($read);
53+
fclose($write);
54+
?>
55+
--EXPECT--
56+
handler
57+
Error: Concurrent access to a stream
58+
Error: Concurrent access to a stream
59+
string(6) "hello
60+
"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
Stream is closed while marked in_use after a fatal error
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--SKIPIF--
7+
<?php
8+
$pair = @stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
9+
if (!$pair) die("skip stream_socket_pair() not available");
10+
if (!function_exists('pcntl_fork')) die("skip pcntl_fork() not available");
11+
?>
12+
--FILE--
13+
<?php
14+
15+
[$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
16+
stream_set_blocking($read, true);
17+
stream_set_timeout($read, 3600);
18+
19+
$pid = pcntl_fork();
20+
if ($pid === 0) {
21+
fwrite($write, "1");
22+
fclose($read);
23+
usleep(200_000); // let parent block in fread()
24+
posix_kill(posix_getppid(), SIGUSR1);
25+
exit(0);
26+
}
27+
28+
pcntl_signal(SIGUSR1, function (int $signo) use ($read): void {
29+
echo "handler\n";
30+
eval('class bool {}');
31+
}, restart_syscalls: true);
32+
33+
register_shutdown_function(function() use ($read) {
34+
try {
35+
fclose($read);
36+
} catch (Error $e) {
37+
echo "shutdown function still cannot access stream: ", $e::class, ": ", $e->getMessage(), "\n";
38+
return;
39+
}
40+
echo "unreachable (shutdown function)\n";
41+
});
42+
43+
// Wait for child to start
44+
fread($read, 1);
45+
46+
$result = fread($read, 1024);
47+
48+
echo "unreachable";
49+
?>
50+
--EXPECTF--
51+
handler
52+
53+
Fatal error: Cannot use "bool" as a class name as it is reserved in %s : eval()'d code on line 1
54+
shutdown function still cannot access stream: Error: Concurrent access to a stream

main/php_streams.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ struct _php_stream_wrapper {
189189

190190
#define PHP_STREAM_FLAG_NO_IO 0x400
191191

192+
/* Stream is in-use: accessing it would be unsafe. This is set when a stream
193+
* operation is interrupted by a signal, before calling signal handlers.
194+
* Stream ops and public stream APIs check this flag before proceeding. */
195+
#define PHP_STREAM_FLAG_IN_USE 0x800
196+
192197
#define PHP_STREAM_FLAG_WAS_WRITTEN 0x80000000
193198

194199
struct _php_stream {
@@ -681,6 +686,32 @@ static inline bool php_is_stream_path(const char *filename)
681686
return ((p != filename) && (p[0] == ':') && (p[1] == '/') && (p[2] == '/'));
682687
}
683688

689+
static inline zend_signal_interrupt_result php_stream_check_signals(php_stream *stream)
690+
{
691+
if (stream != NULL && zend_signal_interrupt_function != zend_signal_interrupt) {
692+
uint32_t orig_in_use = stream->flags & PHP_STREAM_FLAG_IN_USE;
693+
stream->flags |= PHP_STREAM_FLAG_IN_USE;
694+
zend_signal_interrupt_result result = zend_signal_interrupt_function();
695+
stream->flags &= ~PHP_STREAM_FLAG_IN_USE;
696+
stream->flags |= orig_in_use;
697+
return result;
698+
}
699+
700+
return zend_signal_interrupt_function();
701+
}
702+
703+
PHPAPI ZEND_COLD void php_stream_in_use_error(void);
704+
705+
/* See PHP_STREAM_FLAG_IN_USE */
706+
static inline zend_result php_stream_check_in_use(php_stream *stream)
707+
{
708+
if (UNEXPECTED(stream->flags & PHP_STREAM_FLAG_IN_USE)) {
709+
php_stream_in_use_error();
710+
return FAILURE;
711+
}
712+
return SUCCESS;
713+
}
714+
684715
END_EXTERN_C()
685716
#endif
686717

main/streams/streams.c

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ PHPAPI int _php_stream_free(php_stream *stream, int close_options) /* {{{ */
257257
int release_cast = 1;
258258
php_stream_context *context;
259259

260+
/* Not using php_stream_check_in_use() as we want to allow releasing file descriptors during resource shutdown */
261+
if (UNEXPECTED((stream->flags & PHP_STREAM_FLAG_IN_USE) && !(EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN))) {
262+
php_stream_in_use_error();
263+
return 0;
264+
}
265+
260266
/* During shutdown resources may be released before other resources still holding them.
261267
* When only resources are referenced this is not a problem, because they are refcounted
262268
* and will only be fully freed once the refcount drops to zero. However, if php_stream*
@@ -435,6 +441,10 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
435441
{
436442
/* allocate/fill the buffer */
437443

444+
if (php_stream_check_in_use(stream) != SUCCESS) {
445+
return FAILURE;
446+
}
447+
438448
zend_result retval;
439449
bool old_eof = stream->eof;
440450

@@ -607,6 +617,10 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size)
607617
{
608618
ssize_t toread = 0, didread = 0;
609619

620+
if (php_stream_check_in_use(stream) != SUCCESS) {
621+
return 0;
622+
}
623+
610624
while (size > 0) {
611625

612626
/* take from the read buffer first.
@@ -709,6 +723,10 @@ PHPAPI zend_string *php_stream_read_to_str(php_stream *stream, size_t len)
709723

710724
PHPAPI bool _php_stream_eof(php_stream *stream)
711725
{
726+
if (php_stream_check_in_use(stream) != SUCCESS) {
727+
return 0;
728+
}
729+
712730
/* if there is data in the buffer, it's not EOF */
713731
if (stream->writepos - stream->readpos > 0) {
714732
return 0;
@@ -758,6 +776,10 @@ PHPAPI bool _php_stream_puts(php_stream *stream, const char *buf)
758776

759777
PHPAPI int _php_stream_stat(php_stream *stream, php_stream_statbuf *ssb)
760778
{
779+
if (php_stream_check_in_use(stream) != SUCCESS) {
780+
return -1;
781+
}
782+
761783
memset(ssb, 0, sizeof(*ssb));
762784

763785
/* if the stream was wrapped, allow the wrapper to stat it */
@@ -782,6 +804,10 @@ PHPAPI const char *php_stream_locate_eol(php_stream *stream, zend_string *buf)
782804
const char *eol = NULL;
783805
const char *readptr;
784806

807+
if (php_stream_check_in_use(stream) != SUCCESS) {
808+
return 0;
809+
}
810+
785811
if (!buf) {
786812
readptr = (char*)stream->readbuf + stream->readpos;
787813
avail = stream->writepos - stream->readpos;
@@ -827,6 +853,10 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
827853
int grow_mode = 0;
828854
char *bufstart = buf;
829855

856+
if (php_stream_check_in_use(stream) != SUCCESS) {
857+
return NULL;
858+
}
859+
830860
if (buf == NULL) {
831861
grow_mode = 1;
832862
} else if (maxlen == 0) {
@@ -973,6 +1003,10 @@ PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, con
9731003
tent_ret_len; /* tentative returned length */
9741004
bool has_delim = delim_len > 0;
9751005

1006+
if (php_stream_check_in_use(stream) != SUCCESS) {
1007+
return NULL;
1008+
}
1009+
9761010
if (maxlen == 0) {
9771011
return NULL;
9781012
}
@@ -1187,6 +1221,10 @@ PHPAPI int _php_stream_flush(php_stream *stream, int closing)
11871221
{
11881222
int ret = 0;
11891223

1224+
if (php_stream_check_in_use(stream) != SUCCESS) {
1225+
return -1;
1226+
}
1227+
11901228
if (stream->writefilters.head && stream->ops->write) {
11911229
_php_stream_write_filtered(stream, NULL, 0, closing ? PSFS_FLAG_FLUSH_CLOSE : PSFS_FLAG_FLUSH_INC );
11921230
}
@@ -1204,6 +1242,10 @@ PHPAPI ssize_t _php_stream_write(php_stream *stream, const char *buf, size_t cou
12041242
{
12051243
ssize_t bytes;
12061244

1245+
if (php_stream_check_in_use(stream) != SUCCESS) {
1246+
return (ssize_t) -1;
1247+
}
1248+
12071249
if (count == 0) {
12081250
return 0;
12091251
}
@@ -1306,6 +1348,10 @@ static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start
13061348

13071349
PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
13081350
{
1351+
if (php_stream_check_in_use(stream) != SUCCESS) {
1352+
return -1;
1353+
}
1354+
13091355
if (stream->fclose_stdiocast == PHP_STREAM_FCLOSE_FOPENCOOKIE) {
13101356
/* flush can call seek internally so we need to prevent an infinite loop */
13111357
if (!stream->fclose_stdiocast_flush_in_progress) {
@@ -1415,6 +1461,10 @@ PHPAPI int _php_stream_set_option(php_stream *stream, int option, int value, voi
14151461
{
14161462
int ret = PHP_STREAM_OPTION_RETURN_NOTIMPL;
14171463

1464+
if (php_stream_check_in_use(stream) != SUCCESS) {
1465+
return PHP_STREAM_OPTION_RETURN_ERR;
1466+
}
1467+
14181468
if (stream->ops->set_option) {
14191469
ret = stream->ops->set_option(stream, option, value, ptrparam);
14201470
}
@@ -1465,6 +1515,10 @@ PHPAPI ssize_t _php_stream_passthru(php_stream * stream STREAMS_DC)
14651515
char buf[8192];
14661516
ssize_t b;
14671517

1518+
if (php_stream_check_in_use(stream) != SUCCESS) {
1519+
return -1;
1520+
}
1521+
14681522
if (php_stream_mmap_possible(stream)) {
14691523
char *p;
14701524
size_t mapped;
@@ -1506,6 +1560,10 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, bool
15061560
php_stream_statbuf ssbuf;
15071561
zend_string *result;
15081562

1563+
if (php_stream_check_in_use(src) != SUCCESS) {
1564+
return NULL;
1565+
}
1566+
15091567
if (maxlen == 0) {
15101568
return ZSTR_EMPTY_ALLOC();
15111569
}
@@ -1594,7 +1652,6 @@ static zend_result php_stream_copy_fallback(php_stream *src, php_stream *dest, s
15941652
{
15951653
char buf[CHUNK_SIZE];
15961654
size_t haveread = 0;
1597-
15981655
if (maxlen == PHP_STREAM_COPY_ALL) {
15991656
maxlen = 0;
16001657
}
@@ -2240,6 +2297,10 @@ PHPAPI php_stream_context *php_stream_context_set(php_stream *stream, php_stream
22402297
{
22412298
php_stream_context *oldcontext = PHP_STREAM_CONTEXT(stream);
22422299

2300+
if (php_stream_check_in_use(stream) != SUCCESS) {
2301+
return NULL;
2302+
}
2303+
22432304
if (context) {
22442305
stream->ctx = context->res;
22452306
GC_ADDREF(context->res);
@@ -2430,3 +2491,8 @@ PHPAPI int _php_stream_scandir(const char *dirname, zend_string **namelist[], in
24302491
return -1;
24312492
}
24322493
/* }}} */
2494+
2495+
PHPAPI ZEND_COLD void php_stream_in_use_error(void)
2496+
{
2497+
zend_throw_error(NULL, "Concurrent access to a stream");
2498+
}

0 commit comments

Comments
 (0)