Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions ext/zip/php_zip.c
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,12 @@ static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */
}

obj->za = NULL;

if (obj->bailout_callback) {
obj->bailout_callback = false;
zend_bailout();
}

return success;
}
/* }}} */
Expand Down Expand Up @@ -1049,10 +1055,16 @@ static void php_zip_object_dtor(zend_object *object)

if (intern->za) {
if (zip_close(intern->za) != 0) {
php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s", zip_strerror(intern->za));
if (!intern->bailout_callback) {
php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s", zip_strerror(intern->za));
}
zip_discard(intern->za);
}
intern->za = NULL;
if (intern->bailout_callback) {
intern->bailout_callback = false;
zend_bailout();
}
}
}

Expand Down Expand Up @@ -2902,15 +2914,21 @@ PHP_METHOD(ZipArchive, getStream)
#ifdef HAVE_PROGRESS_CALLBACK
static void php_zip_progress_callback(zip_t *arch, double state, void *ptr)
{
if (!EG(active)) {
ze_zip_object *obj = ptr;

if (!EG(active) || obj->bailout_callback) {
return;
}

zval cb_args[1];
ze_zip_object *obj = ptr;

ZVAL_DOUBLE(&cb_args[0], state);
zend_call_known_fcc(&obj->progress_callback, NULL, 1, cb_args, NULL);

zend_try {
zend_call_known_fcc(&obj->progress_callback, NULL, 1, cb_args, NULL);
} zend_catch {
obj->bailout_callback = true;
} zend_end_try();
}

/* {{{ register a progression callback: void callback(double state); */
Expand Down Expand Up @@ -2953,11 +2971,18 @@ static int php_zip_cancel_callback(zip_t *arch, void *ptr)
zval cb_retval;
ze_zip_object *obj = ptr;

if (!EG(active)) {
if (!EG(active) || obj->bailout_callback) {
return 0;
}

zend_call_known_fcc(&obj->cancel_callback, &cb_retval, 0, NULL, NULL);
zend_try {
zend_call_known_fcc(&obj->cancel_callback, &cb_retval, 0, NULL, NULL);
} zend_catch {
obj->bailout_callback = true;
/* Cancel if a bailout occurs to allow cleanup to happen */
return -1;
} zend_end_try();

if (Z_ISUNDEF(cb_retval)) {
/* Cancel if an exception has been thrown */
return -1;
Expand Down
1 change: 1 addition & 0 deletions ext/zip/php_zip.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ typedef struct _ze_zip_object {
zip_int64_t last_id;
int err_zip;
int err_sys;
bool bailout_callback;
#ifdef HAVE_PROGRESS_CALLBACK
zend_fcall_info_cache progress_callback;
#endif
Expand Down
33 changes: 33 additions & 0 deletions ext/zip/tests/gh22176.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
GH-22176 (Memory leak when a ZipArchive cancel callback bails out in the shutdown destructor)
--EXTENSIONS--
zip
--SKIPIF--
<?php
if (!method_exists('ZipArchive', 'registerCancelCallback')) die('skip libzip too old');
?>
--FILE--
<?php
$zip = new ZipArchive;
$zip->open(__DIR__ . '/gh22176_cancel.zip', ZipArchive::CREATE);
$zip->registerCancelCallback(function () {
throw new \Exception('cancel boom');
});
$zip->addFromString('test', 'test');
echo "done\n";
// The archive is flushed and the object destroyed during request shutdown;
// the thrown exception bails out through libzip's zip_close() without leaking
// its internal state, and the bailout resumes once libzip has unwound.
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/gh22176_cancel.zip');
?>
--EXPECTF--
done

Fatal error: Uncaught Exception: cancel boom in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}()
#1 {main}
thrown in %s on line %d
33 changes: 33 additions & 0 deletions ext/zip/tests/gh22176_progress.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
GH-22176 (Memory leak when a ZipArchive progress callback bails out in the shutdown destructor)
--EXTENSIONS--
zip
--SKIPIF--
<?php
if (!method_exists('ZipArchive', 'registerProgressCallback')) die('skip libzip too old');
?>
--FILE--
<?php
$zip = new ZipArchive;
$zip->open(__DIR__ . '/gh22176_progress.zip', ZipArchive::CREATE);
$zip->registerProgressCallback(0.5, function ($r) {
throw new \Exception('progress boom');
});
$zip->addFromString('test', 'test');
echo "done\n";
// The archive is flushed and the object destroyed during request shutdown;
// the thrown exception bails out through libzip's zip_close() without leaking
// its internal state, and the bailout resumes once libzip has unwound.
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/gh22176_progress.zip');
?>
--EXPECTF--
done

Fatal error: Uncaught Exception: progress boom in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(0.0)
#1 {main}
thrown in %s on line %d
Loading