1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

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.
This commit is contained in:
Niels Dossche
2025-06-07 15:29:27 +02:00
parent 066553c454
commit 4b99519eaa
3 changed files with 142 additions and 9 deletions

4
NEWS
View File

@@ -40,6 +40,10 @@ PHP NEWS
. Fix GH-19610 (Deprecation warnings in functions taking as argument).
(Girgias)
- Streams:
. Fixed bug GH-14506 (Closing a userspace stream inside a userspace handler
causes heap corruption). (nielsdos)
- URI:
. Fixed memory management of Uri\WhatWg\Url objects. (timwolla)
. Fixed memory management of the internal "parse_url" URI parser.

View File

@@ -0,0 +1,95 @@
--TEST--
GH-14506 (Closing a userspace stream inside a userspace handler causes heap corruption)
--FILE--
<?php
class Bomb {
public $context;
function stream_open($path, $mode, $options, &$opened_path): bool
{
return true;
}
function stream_write(string $data): int
{
global $readStream;
fclose($readStream);
return 0;
}
function stream_read(int $count): false|string|null
{
global $readStream;
fclose($readStream);
return "";
}
function stream_eof(): bool
{
global $readStream;
fclose($readStream);
return false;
}
function stream_seek(int $offset, int $whence): bool
{
global $readStream;
fclose($readStream);
return false;
}
function stream_cast(int $as)
{
global $readStream;
fclose($readStream);
return false;
}
function stream_flush(): bool
{
global $readStream;
fclose($readStream);
return false;
}
}
stream_register_wrapper('bomb', Bomb::class);
$readStream = fopen('bomb://1', 'r');
fread($readStream, 1);
fwrite($readStream, "x", 1);
fseek($readStream, 0, SEEK_SET);
$streams = [$readStream];
$empty = [];
try {
stream_select($streams, $streams,$empty, 0);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
fflush($readStream);
try {
fclose($readStream);
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: stream_select(): Cannot represent a stream of type user-space as a select()able descriptor in %s on line %d
Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
Warning: stream_select(): Cannot represent a stream of type user-space as a select()able descriptor in %s on line %d
No stream arrays were passed
fclose(): Argument #1 ($stream) must be an open stream resource

View File

@@ -566,6 +566,9 @@ static ssize_t php_userstreamop_write(php_stream *stream, const char *buf, size_
ZVAL_STRINGL(&args[0], (char*)buf, count);
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_WRITE, false);
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
zend_string_release_ex(func_name, false);
@@ -575,6 +578,10 @@ static ssize_t php_userstreamop_write(php_stream *stream, const char *buf, size_
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_WRITE " is not implemented!",
ZSTR_VAL(us->wrapper->ce->name));
}
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= orig_no_fclose;
/* Exception occurred */
if (Z_ISUNDEF(retval)) {
return -1;
@@ -609,28 +616,31 @@ static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count
assert(us != NULL);
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
ZVAL_LONG(&args[0], count);
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_READ, false);
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
zend_string_release_ex(func_name, false);
if (UNEXPECTED(Z_ISUNDEF(retval))) {
return -1;
goto err;
}
if (UNEXPECTED(call_result == FAILURE)) {
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_READ " is not implemented!",
ZSTR_VAL(us->wrapper->ce->name));
return -1;
goto err;
}
if (Z_TYPE(retval) == IS_FALSE) {
return -1;
goto err;
}
if (!try_convert_to_string(&retval)) {
zval_ptr_dtor(&retval);
return -1;
goto err;
}
didread = Z_STRLEN(retval);
@@ -657,11 +667,11 @@ static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count
"%s::" USERSTREAM_EOF " is not implemented! Assuming EOF",
ZSTR_VAL(us->wrapper->ce->name));
stream->eof = 1;
return -1;
goto err;
}
if (UNEXPECTED(Z_ISUNDEF(retval))) {
stream->eof = 1;
return -1;
goto err;
}
if (zval_is_true(&retval)) {
@@ -669,7 +679,15 @@ static ssize_t php_userstreamop_read(php_stream *stream, char *buf, size_t count
}
zval_ptr_dtor(&retval);
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= orig_no_fclose;
return didread;
err:
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= orig_no_fclose;
return -1;
}
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
ZVAL_LONG(&args[0], offset);
ZVAL_LONG(&args[1], whence);
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_SEEK, false);
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 2, args);
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
zval_ptr_dtor(&retval);
return -1;
ret = -1;
goto out;
} else if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF && zval_is_true(&retval)) {
ret = 0;
} else {
@@ -748,7 +770,7 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
ZVAL_UNDEF(&retval);
if (ret) {
return ret;
goto out;
}
/* now determine where we are */
@@ -767,6 +789,11 @@ static int php_userstreamop_seek(php_stream *stream, zend_off_t offset, int when
}
zval_ptr_dtor(&retval);
out:
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= orig_no_fclose;
return ret;
}
@@ -1394,6 +1421,9 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
break;
}
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
zend_string *func_name = ZSTR_INIT_LITERAL(USERSTREAM_CAST, false);
zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 1, args);
zend_string_release_ex(func_name, false);
@@ -1403,7 +1433,7 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
php_error_docref(NULL, E_WARNING, "%s::" USERSTREAM_CAST " is not implemented!",
ZSTR_VAL(us->wrapper->ce->name));
}
return FAILURE;
goto out;
}
do {
@@ -1432,6 +1462,10 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr)
zval_ptr_dtor(&retval);
out:
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= orig_no_fclose;
return ret;
}