Fix GH-22063: stream filter chain UAF on self-removal during callback#22083
Fix GH-22063: stream filter chain UAF on self-removal during callback#22083iliaal wants to merge 1 commit into
Conversation
arnaud-lb
left a comment
There was a problem hiding this comment.
This looks right, but I'm wondering if it is possible to fix this in a way that doesn't force all filter iterators to pay attention to removal.
This affects only user filters, and only when removing themselves.
A related issue with user filters is that they were able to close the stream. This has been fixed by flagging the stream in userfilter_filter().
|
The iterators' Closest alternative: stream-level deferred-free flag + list, each iterator brackets its loop with set/clear/drain. That moves per-iteration logic to per-loop boundaries, I can try that approach if you want? |
|
Another approach would be to leverage refcounting: |
|
Tried the refcount version. Resource dtor on The blocker is request shutdown. Making refcount-only work without that means either the dtor stops being the owner, or filter-resource vs stream-resource teardown order needs coordination, which gives back the simplicity. My failed attempt is here |
|
Thank you for trying! I've tried to fetch your branch, but it looks like that you didn't push it :D I see the issue with shutdown. What we could do is to still remove the filter from the list in stream_filter_remove() as before, so that the dtor's only responsibility is to free the filter. Dtor can ignore filters that are still in a filter chain. This would fix the shutdown issue. However we still get UAFs if the next filter is also removed: class SelfRemovingFilter extends php_user_filter {
public $stream;
public static ?string $key = null;
public static bool $on_closing_only = false;
public function filter($in, $out, &$consumed, $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
if (self::$key !== null && (!self::$on_closing_only || $closing)) {
$res = $GLOBALS[self::$key];
self::$key = null;
stream_filter_remove($res);
stream_filter_remove($GLOBALS['filter2']);
}
return PSFS_PASS_ON;
}
}
stream_filter_register('self-removing', SelfRemovingFilter::class);
echo "write side: ";
$f = fopen('php://memory', 'r+');
SelfRemovingFilter::$key = 'write_res';
SelfRemovingFilter::$on_closing_only = false;
$GLOBALS['write_res'] = stream_filter_append($f, 'self-removing', STREAM_FILTER_WRITE);
$GLOBALS['filter2'] = stream_filter_append($f, 'string.rot13', STREAM_FILTER_WRITE);
fwrite($f, 'hello');
fwrite($f, ' world');
rewind($f);
echo stream_get_contents($f), "\n";Maybe we should just deny the removal of filter during filtering. |
A php_user_filter that calls stream_filter_remove() from inside its own filter() callback frees the php_stream_filter struct while the chain walker in _php_stream_filter_flush, _php_stream_write_filtered, and _php_stream_fill_read_buffer still holds it via current->next. Carry an in_iteration counter on php_stream_filter_chain that the four dispatch sites bracket around their loops; stream_filter_remove() refuses with E_WARNING + FALSE while the counter is non-zero, which also covers a callback removing a different filter on the same chain. Fixes phpGH-22063
|
It helps to actually push branches, who would've guessed 🤦 https://github.com/iliaal/php-src/tree/alt/gh-22063-refcount I think the idea of denying the removal of filter during filtering is the right path, I am re-working the PR to do that. |
|
If we don't want to deny removal of filters during filtering, your approach is probably the best one. To avoid the
|
927ccd7 to
0f8079b
Compare
A
php_user_filterthat callsstream_filter_removeon its own resource from insidefilter()frees thephp_stream_filterstruct whileuserfilter_filterstill dereferences&thisfilter->abstractand while the chain walks in_php_stream_filter_flush,_php_stream_write_filtered, and_php_stream_fill_read_bufferstill needcurrent->next. Two new fields onphp_stream_filter,in_callbackanddeferred_dtor, carry the deferral:php_stream_filter_remove(filter, true)unlinks and runszend_list_deleteimmediately but defers thepefree.