Skip to content

Commit 3db0487

Browse files
committed
Don't expose a freed stream resource to user filters
When a stream is freed from its resource destructor, the on-close write-filter flush runs the user filter callback while the stream's resource is already dtor'd (type == -1) and about to be freed. Exposing it through $this->stream let user code capture the dead resource in an exception backtrace, a use-after-free. Assign null when the resource is no longer live; the explicit fclose() flush still runs before the resource is closed, so live streams are unaffected. Fixes GH-15836 Closes GH-22503
1 parent 889ae47 commit 3db0487

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ PHP NEWS
1919
- Sockets:
2020
. Fixed various memory related issues in ext/sockets. (David Carlier)
2121

22+
- Streams:
23+
. Fixed bug GH-15836 (Use-after-free when a user stream filter accesses
24+
$this->stream during the close flush). (iliaal)
25+
2226
30 Jul 2026, PHP 8.4.24
2327

2428
- Calendar:

ext/standard/tests/filters/bug54350.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ fwrite($fd, "foo");
2323
?>
2424
--EXPECTF--
2525
Warning: fclose(): 5 is not a valid stream resource in %s on line %d
26-
fclose(): supplied resource is not a valid stream resource
26+
fclose(): Argument #1 ($stream) must be of type resource, null given
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
GH-15836 (use-after-free when a user filter reads $this->stream during the close flush)
3+
--FILE--
4+
<?php
5+
class my_filter extends php_user_filter {
6+
public static ?Throwable $e = null;
7+
function filter($in, $out, &$consumed, $closing): int {
8+
if ($closing) {
9+
try {
10+
stream_bucket_new($this->stream, "x");
11+
} catch (TypeError $e) {
12+
self::$e = $e;
13+
}
14+
}
15+
return PSFS_PASS_ON;
16+
}
17+
}
18+
var_dump(stream_filter_register("my_filter", "my_filter"));
19+
20+
function run() {
21+
$s = fopen("php://memory", "wb+");
22+
stream_filter_append($s, "my_filter", STREAM_FILTER_WRITE);
23+
}
24+
run();
25+
26+
echo my_filter::$e->getTraceAsString(), "\n";
27+
echo "done\n";
28+
?>
29+
--EXPECTF--
30+
bool(true)
31+
#0 %s(%d): stream_bucket_new(NULL, 'x')
32+
#1 %s(%d): my_filter->filter(Resource id #%d, Resource id #%d, 0, true)
33+
#2 {main}
34+
done

ext/standard/user_filters.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@ static php_stream_filter_status_t userfilter_filter(
156156
bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
157157
if (stream_property_exists) {
158158
zval stream_zval;
159-
php_stream_to_zval(stream, &stream_zval);
159+
if (EXPECTED(stream->res && stream->res->type >= 0)) {
160+
php_stream_to_zval(stream, &stream_zval);
161+
} else {
162+
ZVAL_NULL(&stream_zval);
163+
}
160164
zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
161165
/* If property update threw an exception, skip filter execution */
162166
if (EG(exception)) {

0 commit comments

Comments
 (0)