Repository navigation
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. |
|
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
|
Pushed one more commit. |
bukka
left a comment
There was a problem hiding this comment.
I think the prohibiting approach makes sense consider that we have a similar no fclose approach for the actual stream.
You should also change PR description.
|
|
||
| /* Owning stream */ | ||
| php_stream *stream; | ||
| uint32_t in_iteration; |
There was a problem hiding this comment.
Do we really need to break ABI here? I mean we could use a similar logic as for stream protection using PHP_STREAM_FLAG_NO_FCLOSE (in this case 2 bits as we have got two chains). Then we could consider 8.4 as a target. It's also a bit more consistent.
There was a problem hiding this comment.
Switched to two stream flags, saved and restored around each chain walk like NO_FCLOSE, and retargeted to PHP-8.4. stream_filter_remove() also holds the flag across its own flush, since a wrapper's stream_write() could otherwise remove the same filter mid-removal.
bukka
left a comment
There was a problem hiding this comment.
Also needs resolving NEWS conflict...
Calling stream_filter_remove() from a user filter callback, or from a stream wrapper write issued by another removal's flush, frees a php_stream_filter that C code up the stack still uses. Deny removal while the filter's chain is being applied or flushed, tracked by one stream flag per chain so the filter structs keep their layout. Fixes phpGH-22063
ea30cf5 to
0aa3b69
Compare
A
php_user_filterthat callsstream_filter_remove()while its chain is being applied frees a filter that the chain walk, oruserfilter_filter()after the callback returns, still uses.stream_filter_remove()now warns and returns false in that window. The state lives in two stream flags, one per chain, set around each chain walk and around the removal's own flush, so no struct changes and the fix can target PHP-8.4.PHP-8.6 added filter
seek(), so when merging up,php_stream_filters_seek()needs the same bracket plusgh22063_seek.phpt, both on https://github.com/iliaal/php-src/tree/fix/gh-22063-flags-master.Fixes GH-22063