Skip to content

Commit 927ccd7

Browse files
committed
Fix GH-22063: stream filter chain UAF on self-removal during callback
A stream filter struct must stay live while a fops->filter() callback or chain iteration holds it. A php_user_filter that removes its own resource inside filter() frees the struct under userfilter_filter (&thisfilter->abstract deref) and under the three chain-walk sites (current->next read). Defer pefree via an in_callback counter until every C-level frame holding the filter releases it. Closes GH-22063
1 parent 7827754 commit 927ccd7

5 files changed

Lines changed: 101 additions & 4 deletions

File tree

ext/standard/streamsfuncs.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1304,11 +1304,23 @@ PHP_FUNCTION(stream_filter_remove)
13041304
RETURN_THROWS();
13051305
}
13061306

1307-
if (php_stream_filter_flush(filter, 1) == FAILURE) {
1307+
filter->in_callback++;
1308+
zend_result flush_result = php_stream_filter_flush(filter, 1);
1309+
filter->in_callback--;
1310+
1311+
if (flush_result == FAILURE) {
1312+
if (filter->deferred_dtor && filter->in_callback == 0) {
1313+
php_stream_filter_free(filter);
1314+
}
13081315
php_error_docref(NULL, E_WARNING, "Unable to flush filter, not removing");
13091316
RETURN_FALSE;
13101317
}
13111318

1319+
if (filter->deferred_dtor && filter->in_callback == 0) {
1320+
php_stream_filter_free(filter);
1321+
RETURN_TRUE;
1322+
}
1323+
13121324
zend_list_close(Z_RES_P(zfilter));
13131325
php_stream_filter_remove(filter, 1);
13141326
RETURN_TRUE;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
GH-22063 (Stream filter chain UAF via self-removal during callback)
3+
--FILE--
4+
<?php
5+
class SelfRemovingFilter extends php_user_filter {
6+
public $stream;
7+
public static ?string $key = null;
8+
public static bool $on_closing_only = false;
9+
10+
public function filter($in, $out, &$consumed, $closing): int
11+
{
12+
while ($bucket = stream_bucket_make_writeable($in)) {
13+
$consumed += $bucket->datalen;
14+
stream_bucket_append($out, $bucket);
15+
}
16+
if (self::$key !== null && (!self::$on_closing_only || $closing)) {
17+
$res = $GLOBALS[self::$key];
18+
self::$key = null;
19+
stream_filter_remove($res);
20+
}
21+
return PSFS_PASS_ON;
22+
}
23+
}
24+
25+
stream_filter_register('self-removing', SelfRemovingFilter::class);
26+
27+
echo "write side: ";
28+
$f = fopen('php://memory', 'r+');
29+
SelfRemovingFilter::$key = 'write_res';
30+
SelfRemovingFilter::$on_closing_only = false;
31+
$GLOBALS['write_res'] = stream_filter_append($f, 'self-removing', STREAM_FILTER_WRITE);
32+
fwrite($f, 'hello');
33+
fwrite($f, ' world');
34+
rewind($f);
35+
echo stream_get_contents($f), "\n";
36+
37+
echo "read side: ";
38+
$f = fopen('php://memory', 'r+');
39+
fwrite($f, 'abcdefghij');
40+
rewind($f);
41+
SelfRemovingFilter::$key = 'read_res';
42+
SelfRemovingFilter::$on_closing_only = false;
43+
$GLOBALS['read_res'] = stream_filter_append($f, 'self-removing', STREAM_FILTER_READ);
44+
echo fread($f, 4), '|', fread($f, 6), "\n";
45+
46+
echo "closing-flush side: ";
47+
$f = fopen('php://memory', 'r+');
48+
SelfRemovingFilter::$key = 'close_res';
49+
SelfRemovingFilter::$on_closing_only = true;
50+
$GLOBALS['close_res'] = stream_filter_append($f, 'self-removing', STREAM_FILTER_WRITE);
51+
stream_filter_remove($GLOBALS['close_res']);
52+
echo "ok\n";
53+
?>
54+
--EXPECT--
55+
write side: hello world
56+
read side: abcd|efghij
57+
closing-flush side: ok

main/streams/filter.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,17 @@ PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool fini
465465
chain = filter->chain;
466466
stream = chain->stream;
467467

468-
for(current = filter; current; current = current->next) {
468+
php_stream_filter *next_filter;
469+
for (current = filter; current; current = next_filter) {
469470
php_stream_filter_status_t status;
470471

472+
next_filter = current->next;
473+
current->in_callback++;
471474
status = current->fops->filter(stream, current, inp, outp, NULL, flags);
475+
current->in_callback--;
476+
if (current->deferred_dtor && current->in_callback == 0) {
477+
php_stream_filter_free(current);
478+
}
472479
if (status == PSFS_FEED_ME) {
473480
/* We've flushed the data far enough */
474481
return SUCCESS;
@@ -550,6 +557,10 @@ PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bo
550557
}
551558

552559
if (call_dtor) {
560+
if (filter->in_callback) {
561+
filter->deferred_dtor = true;
562+
return NULL;
563+
}
553564
php_stream_filter_free(filter);
554565
return NULL;
555566
}

main/streams/php_stream_filter_api.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ struct _php_stream_filter {
130130

131131
/* filters are auto_registered when they're applied */
132132
zend_resource *res;
133+
134+
uint32_t in_callback;
135+
bool deferred_dtor;
133136
};
134137

135138
/* stack filter onto a stream */

main/streams/streams.c

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,15 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
580580
}
581581

582582
/* wind the handle... */
583-
for (filter = stream->readfilters.head; filter; filter = filter->next) {
583+
php_stream_filter *next_filter;
584+
for (filter = stream->readfilters.head; filter; filter = next_filter) {
585+
next_filter = filter->next;
586+
filter->in_callback++;
584587
status = filter->fops->filter(stream, filter, brig_inp, brig_outp, NULL, flags);
588+
filter->in_callback--;
589+
if (filter->deferred_dtor && filter->in_callback == 0) {
590+
php_stream_filter_free(filter);
591+
}
585592

586593
if (status != PSFS_PASS_ON) {
587594
break;
@@ -1233,11 +1240,18 @@ static ssize_t _php_stream_write_filtered(php_stream *stream, const char *buf, s
12331240
php_stream_bucket_append(&brig_in, bucket);
12341241
}
12351242

1236-
for (php_stream_filter *filter = stream->writefilters.head; filter; filter = filter->next) {
1243+
php_stream_filter *next_filter;
1244+
for (php_stream_filter *filter = stream->writefilters.head; filter; filter = next_filter) {
1245+
next_filter = filter->next;
12371246
/* for our return value, we are interested in the number of bytes consumed from
12381247
* the first filter in the chain */
1248+
filter->in_callback++;
12391249
status = filter->fops->filter(stream, filter, brig_inp, brig_outp,
12401250
filter == stream->writefilters.head ? &consumed : NULL, flags);
1251+
filter->in_callback--;
1252+
if (filter->deferred_dtor && filter->in_callback == 0) {
1253+
php_stream_filter_free(filter);
1254+
}
12411255

12421256
if (status != PSFS_PASS_ON) {
12431257
break;

0 commit comments

Comments
 (0)