Skip to content

Commit ea30cf5

Browse files
committed
Also deny filter removal during a seek() callback
php_stream_filters_seek walks the filter chain calling filter->fops->seek without raising the chain in_iteration counter, so a user filter whose seek() method removes itself frees the filter mid-walk and the subsequent filter->next / userfilter_seek property write are use-after-free. Bracket the seek loop the same way as the other chain walks so a self-removal during a seek() callback is denied instead of freeing the live filter.
1 parent 0f8079b commit ea30cf5

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
GH-22063 (stream filter self-removal denied during a seek() callback)
3+
--FILE--
4+
<?php
5+
class SeekRemover extends php_user_filter {
6+
public function filter($in, $out, &$consumed, $closing): int {
7+
while ($b = stream_bucket_make_writeable($in)) {
8+
stream_bucket_append($out, $b);
9+
}
10+
return PSFS_PASS_ON;
11+
}
12+
public function seek(int $offset, int $whence, int $chain): bool {
13+
// Removing the filter that is mid-seek must be denied, not a UAF.
14+
var_dump(stream_filter_remove($GLOBALS['f']));
15+
return true;
16+
}
17+
}
18+
stream_filter_register('seekremover', SeekRemover::class);
19+
20+
$fp = fopen('php://memory', 'r+');
21+
fwrite($fp, 'hello world data here');
22+
rewind($fp);
23+
$GLOBALS['f'] = stream_filter_append($fp, 'seekremover', STREAM_FILTER_READ);
24+
var_dump(fread($fp, 4));
25+
var_dump(fseek($fp, 0));
26+
echo "done\n";
27+
?>
28+
--EXPECTF--
29+
string(4) "hell"
30+
31+
Warning: stream_filter_remove(): Cannot remove filter while it is being applied in %s on line %d
32+
bool(false)
33+
int(0)
34+
done

‎main/streams/streams.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,17 +1385,27 @@ static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_s
13851385
static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter,
13861386
bool is_start_seeking, zend_off_t offset, int whence, int chain_type)
13871387
{
1388+
php_stream_filter_chain *chain = filter ? filter->chain : NULL;
1389+
if (chain) {
1390+
chain->in_iteration++;
1391+
}
13881392
while (filter) {
13891393
php_stream_filter_seekable_t seekable = (chain_type == PHP_STREAM_FILTER_READ) ?
13901394
filter->read_seekable : filter->write_seekable;
13911395
if (((seekable == PSFS_SEEKABLE_START && is_start_seeking) ||
13921396
seekable == PSFS_SEEKABLE_CHECK) &&
13931397
filter->fops->seek(stream, filter, offset, whence) == FAILURE) {
13941398
php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label);
1399+
if (chain) {
1400+
chain->in_iteration--;
1401+
}
13951402
return FAILURE;
13961403
}
13971404
filter = filter->next;
13981405
}
1406+
if (chain) {
1407+
chain->in_iteration--;
1408+
}
13991409
return SUCCESS;
14001410
}
14011411

0 commit comments

Comments
 (0)