Skip to content

Commit 4b99519

Browse files
committed
Fix GH-14506: Closing a userspace stream inside a userspace handler causes heap corruption
Use the PHP_STREAM_FLAG_NO_FCLOSE flag to prevent closing a stream while a handler is running. We already do this in some other places as well. Only handlers that do something with the stream afterwards need changes. Closes GH-18797.
1 parent 066553c commit 4b99519

File tree

3 files changed

+142
-9
lines changed

3 files changed

+142
-9
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ PHP NEWS
4040
. Fix GH-19610 (Deprecation warnings in functions taking as argument).
4141
(Girgias)
4242

43+
- Streams:
44+
. Fixed bug GH-14506 (Closing a userspace stream inside a userspace handler
45+
causes heap corruption). (nielsdos)
46+
4347
- URI:
4448
. Fixed memory management of Uri\WhatWg\Url objects. (timwolla)
4549
. Fixed memory management of the internal "parse_url" URI parser.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
--TEST--
2+
GH-14506 (Closing a userspace stream inside a userspace handler causes heap corruption)
3+
--FILE--
4+
<?php
5+
6+
class Bomb {
7+
8+
public $context;
9+
10+
function stream_open($path, $mode, $options, &$opened_path): bool
11+
{
12+
return true;
13+
}
14+
15+
function stream_write(string $data): int
16+
{
17+
global $readStream;
18+
fclose($readStream);
19+
return 0;
20+
}
21+
22+
function stream_read(int $count): false|string|null
23+
{
24+
global $readStream;
25+
fclose($readStream);
26+
return "";
27+
}
28+
29+
function stream_eof(): bool
30+
{
31+
global $readStream;
32+
fclose($readStream);
33+
return false;
34+
}
35+
36+
function stream_seek(int $offset, int $whence): bool
37+
{
38+
global $readStream;
39+
fclose($readStream);
40+
return false;
41+
}
42+
43+
function stream_cast(int $as)
44+
{
45+
global $readStream;
46+
fclose($readStream);
47+
return false;
48+
}
49+
50+
function stream_flush(): bool
51+
{
52+
global $readStream;
53+
fclose($readStream);
54+
return false;
55+
}
56+
}
57+
58+
stream_register_wrapper('bomb', Bomb::class);
59+
$readStream = fopen('bomb://1', 'r');
60+
fread($readStream, 1);
61+
fwrite($readStream, "x", 1);
62+
fseek($readStream, 0, SEEK_SET);
63+
$streams = [$readStream];
64+
$empty = [];
65+
try {
66+
stream_select($streams, $streams,$empty, 0);
67+
} catch (ValueError $e) {
68+
echo $e->getMessage(), "\n";
69+
}
70+
fflush($readStream);
71+
try {
72+
fclose($readStream);
73+
} catch (TypeError $e) {
74+
echo $e->getMessage(), "\n";
75+
}
76+
77+
?>
78+
--EXPECTF--
79+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
80+
81+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
82+
83+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
84+
85+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
86+
87+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
88+
89+
Warning: stream_select(): Cannot represent a stream of type user-space as a select()able descriptor in %s on line %d
90+
91+
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
92+
93+
Warning: stream_select(): Cannot represent a stream of type user-space as a select()able descriptor in %s on line %d
94+
No stream arrays were passed
95+
fclose(): Argument #1 ($stream) must be an open stream resource

main/streams/userspace.c

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,9 @@ static ssize_t php_userstreamop_write(php_stream *stream, const char *buf, size_
566566

567567
ZVAL_STRINGL(&args[0], (char*)buf, count);
568568

569+
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
570+
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
571+
569572
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_WRITE, false);
570573
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
571574
zend_string_release_ex(func_name, false);
@@ -575,6 +578,10 @@ static ssize_t php_userstreamop_write(php_stream *stream, const char *buf, size_
575578
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_WRITE " is not implemented!",
576579
ZSTR_VAL(us->wrapper->ce->name));
577580
}
581+
582+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
583+
stream->flags |= orig_no_fclose;
584+
578585
/* Exception occurred */
579586
if (Z_ISUNDEF(retval)) {
580587
return -1;
@@ -609,28 +616,31 @@ static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count
609616

610617
assert(us != NULL);
611618

619+
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
620+
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
621+
612622
ZVAL_LONG(&args[0], count);
613623
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_READ, false);
614624
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
615625
zend_string_release_ex(func_name, false);
616626

617627
if (UNEXPECTED(Z_ISUNDEF(retval))) {
618-
return -1;
628+
goto err;
619629
}
620630

621631
if (UNEXPECTED(call_result == FAILURE)) {
622632
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_READ " is not implemented!",
623633
ZSTR_VAL(us->wrapper->ce->name));
624-
return -1;
634+
goto err;
625635
}
626636

627637
if (Z_TYPE(retval) == IS_FALSE) {
628-
return -1;
638+
goto err;
629639
}
630640

631641
if (!try_convert_to_string(&retval)) {
632642
zval_ptr_dtor(&retval);
633-
return -1;
643+
goto err;
634644
}
635645

636646
didread = Z_STRLEN(retval);
@@ -657,19 +667,27 @@ static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count
657667
"%s::" USERSTREAM_EOF " is not implemented! Assuming EOF",
658668
ZSTR_VAL(us->wrapper->ce->name));
659669
stream->eof = 1;
660-
return -1;
670+
goto err;
661671
}
662672
if (UNEXPECTED(Z_ISUNDEF(retval))) {
663673
stream->eof = 1;
664-
return -1;
674+
goto err;
665675
}
666676

667677
if (zval_is_true(&retval)) {
668678
stream->eof = 1;
669679
}
670680
zval_ptr_dtor(&retval);
671681

682+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
683+
stream->flags |= orig_no_fclose;
684+
672685
return didread;
686+
687+
err:
688+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
689+
stream->flags |= orig_no_fclose;
690+
return -1;
673691
}
674692

675693
static int php_userstreamop_close(php_stream *stream, int close_handle)
@@ -723,6 +741,9 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
723741
ZVAL_LONG(&args[0], offset);
724742
ZVAL_LONG(&args[1], whence);
725743

744+
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
745+
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
746+
726747
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_SEEK, false);
727748
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 2, args);
728749
zend_string_release_ex(func_name, false);
@@ -737,7 +758,8 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
737758

738759
zval_ptr_dtor(&retval);
739760

740-
return -1;
761+
ret = -1;
762+
goto out;
741763
} else if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF && zval_is_true(&retval)) {
742764
ret = 0;
743765
} else {
@@ -748,7 +770,7 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
748770
ZVAL_UNDEF(&retval);
749771

750772
if (ret) {
751-
return ret;
773+
goto out;
752774
}
753775

754776
/* now determine where we are */
@@ -767,6 +789,11 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
767789
}
768790

769791
zval_ptr_dtor(&retval);
792+
793+
out:
794+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
795+
stream->flags |= orig_no_fclose;
796+
770797
return ret;
771798
}
772799

@@ -1394,6 +1421,9 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
13941421
break;
13951422
}
13961423

1424+
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
1425+
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
1426+
13971427
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_CAST, false);
13981428
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
13991429
zend_string_release_ex(func_name, false);
@@ -1403,7 +1433,7 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
14031433
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_CAST " is not implemented!",
14041434
ZSTR_VAL(us->wrapper->ce->name));
14051435
}
1406-
return FAILURE;
1436+
goto out;
14071437
}
14081438

14091439
do {
@@ -1432,6 +1462,10 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
14321462

14331463
zval_ptr_dtor(&retval);
14341464

1465+
out:
1466+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
1467+
stream->flags |= orig_no_fclose;
1468+
14351469
return ret;
14361470
}
14371471

0 commit comments

Comments
 (0)