Fix GH-23276: Collect ZipArchive subclasses holding their own streams - #23282
Fix GH-23276: Collect ZipArchive subclasses holding their own streams#23282LamentXU123 wants to merge 2 commits into
Conversation
It is probably best for a stable branch IMO, that being said I ll review it sometime this weekend. |
|
I would suggest one thing after a quick look. Keep at it, this refcounting approach is likely to cause indirect issues. |
|
I think this is wrong, I believe you need a get_gc object handler for ZipArchive. |
|
Apparently I have to take some time to study Zend GC. It seems like we don't have a reasonable way to bypass this entirely. |
| * releasing it again would underflow the refcount. A non-zero | ||
| * refcount means that this stream is being closed normally. | ||
| */ | ||
| if (GC_REFCOUNT(&self->owner->zo) != 0) { |
There was a problem hiding this comment.
I ll see the rest later but what happens if a php globals still holds the resource then we fclose on it for ex ?
There was a problem hiding this comment.
I think its fine at the first glance. Let's add a regression test just in case.
There was a problem hiding this comment.
came up with this
<?php
class Reader extends ZipArchive {
public $stream;
public function __destruct() { $GLOBALS['zombie'] = $this; }
}
$f = __DIR__ . '/n5s.zip';
$z = new ZipArchive;
$z->open($f, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$z->addFromString('entry.txt', 'contents');
$z->close();
$zip = new Reader;
$zip->open($f, ZipArchive::RDONLY);
$zip->stream = $zip->getStream('entry.txt');
unset($zip);
gc_collect_cycles();
var_dump($GLOBALS['zombie'] instanceof ZipArchive);
var_dump($GLOBALS['zombie']->stream);
echo 'done', PHP_EOL;
@unlink($f);There was a problem hiding this comment.
I ll see the rest later but what happens if a php globals still holds the resource then we fclose on it for ex ?
You are correct. ZipArchive's owner pin doesn't delete the object. Thanks.
|
something tells me it s probably best to keep it as draft for now, I did not review the whole code yet but already unsure everything is fully correct. |
|
|
||
| zend_gc_get_status(&gc_status); | ||
| zend_hash_init(&streams, 0, NULL, NULL, 0); | ||
| ZEND_HASH_FOREACH_VAL_IND(properties, value) { |
There was a problem hiding this comment.
that seems overall costly performance wise. Prior, get_gc did absolutely nothing, that s a big jump not in a nice way.
|
I would like Nora opinion on the get_gc topic, not feeling confident about it. It feels overall fragile but also borderline master-ish level of changes for a stable branch. |
|
I tried experimenting with this, but after all, I think that the original patch (the one introducing this issue) was not entirely right. What's especially painful is that it pins a resource to an object, something not well-supported by the engine (e.g. gc_mark_grey is not supported for IS_RESOURCE). Especially fun is how resources and objects interact with each other during shutdown. However, this causes a problem: we need to refactor how the objects are layed out I'm afraid... So my proposal, whilst IMO cleaner and more correct, is quite heavy of a change: https://gist.github.com/ndossche/8da9b0d6a323a4ad411844b62e37f33b If you don't wish this heavy patch to be applied to 8.4, so be it, but then the original fix should also be reverted as I think that breaking the engine assumptions are more problematic. |
Yes that's exactly my point. Ideally, there should be a fix in the Zend engine, however I don't think my knowledge to the engine can support me to write the patch. Also, I don't want to bother other behaviors in stable branches.
I don't think the original patch is correct (I didn't realize this issue when reviewing) so I have no objections on reverting the original patch, it should be done. The interesting thing is, my first version of patch is similar to yours, and then I change it because I thought it might be too heavy and I am not fully confident on it, so I finally chose to use get_gc weirdly here. So this basically makes the two of us. This patch is to a degree a good refactoring, and I fully agree to it. |
|
Branch-wise, IMO this should be done in 8.4 no matter how we fix this. Because the problematic patch is merged in lower branches, so the fix should be merged in lower branches. |
Keep the native archive state alive independently of the ZipArchive object so streams remain readable without pinning the object. Co-authored-by: ndossche <7771979+ndossche@users.noreply.github.com>
|
zip callbacks are a special case, could you try locally (with a debug build I do not think you need sanitizers) $zip = new ZipArchive;
$zip->open($filename);
$zip->registerProgressCallback(0.5, function ($rate) {}); // same with cancel one
$zip->addFromString('late.txt', 'late');
$stream = $zip->getStream('entry.txt');
unset($zip); |
|
Yes ze_zip_object is freed in ur case but the stream preserve the zip_t. So when zip_close we got a UAF. |
I actually doubt this is a bug in the Zend engine. But since I am not familiar with how Zend GC works, I'd fix this is in a completely ext/zip way, to replace the stream's reference to the
ZipArchiveobject with a refcounted archive state, so a stream can keep the underlyingzip_twithout preventing its owningZipArchiveobject from being GC-ed.There can be better fixes, as I said, to fix in the Zend engine. But I think this is a very rare case happening in the zip extension, so I'd rather re-implement it than touching the Zend core, so we don't bother other code.
Fixes GH-23276