diff --git a/NEWS b/NEWS index d3b9b62ce8b2..0265ad778095 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,10 @@ PHP NEWS - Readline: . Fixed class constant completion in the interactive shell. (Weilin Du) +- Zip: + . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive + is freed while the stream is still open). (Eyüp Can Akman) + 13 Aug 2026, PHP 8.6.0beta1 - Core: diff --git a/ext/com_dotnet/com_com.c b/ext/com_dotnet/com_com.c index 63fc8f6563b2..e0fbb0c17399 100644 --- a/ext/com_dotnet/com_com.c +++ b/ext/com_dotnet/com_com.c @@ -670,7 +670,8 @@ PHP_FUNCTION(com_create_guid) /* {{{ Connect events from a COM object to a PHP object */ PHP_FUNCTION(com_event_sink) { - zval *object, *sinkobject; + zend_object *object; + zend_object *sinkobject; zend_string *sink_str = NULL; HashTable *sink_ht = NULL; zend_string *type_lib_name = NULL; @@ -679,8 +680,8 @@ PHP_FUNCTION(com_event_sink) ITypeInfo *typeinfo = NULL; ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_OBJECT_OF_CLASS(object, php_com_variant_class_entry) - Z_PARAM_OBJECT(sinkobject) + Z_PARAM_OBJ_OF_CLASS(object, php_com_variant_class_entry) + Z_PARAM_OBJ(sinkobject) Z_PARAM_OPTIONAL Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(sink_ht, sink_str) ZEND_PARSE_PARAMETERS_END(); @@ -688,7 +689,7 @@ PHP_FUNCTION(com_event_sink) RETVAL_FALSE; php_com_initialize(); - obj = CDNO_FETCH(object); + obj = (php_com_dotnet_object*)object; if (sink_ht) { /* 0 => typelibname, 1 => dispname */ diff --git a/ext/com_dotnet/com_variant.c b/ext/com_dotnet/com_variant.c index 82f17d12d9b5..87d33da1daf4 100644 --- a/ext/com_dotnet/com_variant.c +++ b/ext/com_dotnet/com_variant.c @@ -115,7 +115,7 @@ static void php_com_variant_from_zval_ex(VARIANT *v, zval *z, int codepage, VART break; case IS_OBJECT: - if (php_com_is_valid_object(z)) { + if (php_com_is_valid_object(Z_OBJ_P(z))) { obj = CDNO_FETCH(z); if (V_VT(&obj->v) == VT_DISPATCH) { /* pass the underlying object */ @@ -132,7 +132,7 @@ static void php_com_variant_from_zval_ex(VARIANT *v, zval *z, int codepage, VART } else { /* export the PHP object using our COM wrapper */ V_VT(v) = VT_DISPATCH; - V_DISPATCH(v) = php_com_wrapper_export(z); + V_DISPATCH(v) = php_com_wrapper_export(Z_OBJ_P(z)); } break; diff --git a/ext/com_dotnet/com_wrapper.c b/ext/com_dotnet/com_wrapper.c index 175a2de33074..a040c0a84aba 100644 --- a/ext/com_dotnet/com_wrapper.c +++ b/ext/com_dotnet/com_wrapper.c @@ -499,11 +499,11 @@ static void generate_dispids(php_dispatchex *disp) } } -static php_dispatchex *disp_constructor(zval *object) +static php_dispatchex *disp_constructor(zend_object *object) { php_dispatchex *disp = (php_dispatchex*)CoTaskMemAlloc(sizeof(php_dispatchex)); - trace("constructing a COM wrapper for PHP object %p (%s)\n", object, ZSTR_VAL(Z_OBJCE_P(object)->name)); + trace("constructing a COM wrapper for PHP object %p (%s)\n", object, ZSTR_VAL(object->ce->name)); if (disp == NULL) return NULL; @@ -516,7 +516,7 @@ static php_dispatchex *disp_constructor(zval *object) if (object) { - ZVAL_COPY(&disp->object, object); + ZVAL_OBJ_COPY(&disp->object, object); } else { ZVAL_UNDEF(&disp->object); } @@ -536,7 +536,7 @@ static void disp_destructor(php_dispatchex *disp) CoTaskMemFree(disp); } -PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export_as_sink(zval *val, GUID *sinkid, +PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export_as_sink(zend_object *val, GUID *sinkid, HashTable *id_to_name) { php_dispatchex *disp = disp_constructor(val); @@ -572,17 +572,13 @@ PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export_as_sink(zval *val, GUID *si return (IDispatch*)disp; } -PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export(zval *val) +PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export(zend_object *val) { php_dispatchex *disp = NULL; - if (Z_TYPE_P(val) != IS_OBJECT) { - return NULL; - } - if (php_com_is_valid_object(val)) { /* pass back its IDispatch directly */ - php_com_dotnet_object *obj = CDNO_FETCH(val); + php_com_dotnet_object *obj = (php_com_dotnet_object*)val; if (obj == NULL) return NULL; diff --git a/ext/com_dotnet/php_com_dotnet_internal.h b/ext/com_dotnet/php_com_dotnet_internal.h index c7e065c24872..64a1884244ca 100644 --- a/ext/com_dotnet/php_com_dotnet_internal.h +++ b/ext/com_dotnet/php_com_dotnet_internal.h @@ -44,9 +44,9 @@ typedef struct _php_com_dotnet_object { HashTable *id_of_name_cache; } php_com_dotnet_object; -static inline bool php_com_is_valid_object(zval *zv) +static inline bool php_com_is_valid_object(zend_object *obj) { - zend_class_entry *ce = Z_OBJCE_P(zv); + const zend_class_entry *ce = obj->ce; return zend_string_equals_literal(ce->name, "com") || zend_string_equals_literal(ce->name, "dotnet") || zend_string_equals_literal(ce->name, "variant"); @@ -54,7 +54,7 @@ static inline bool php_com_is_valid_object(zval *zv) #define CDNO_FETCH(zv) (php_com_dotnet_object*)Z_OBJ_P(zv) #define CDNO_FETCH_VERIFY(obj, zv) do { \ - if (!php_com_is_valid_object(zv)) { \ + if (!php_com_is_valid_object(Z_OBJ_P(zv))) { \ php_com_throw_exception(E_UNEXPECTED, "expected a variant object"); \ return; \ } \ @@ -99,8 +99,8 @@ zend_result php_com_do_invoke_byref(php_com_dotnet_object *obj, zend_internal_fu WORD flags, VARIANT *v, int nargs, zval *args); /* com_wrapper.c */ -PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export_as_sink(zval *val, GUID *sinkid, HashTable *id_to_name); -PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export(zval *val); +PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export_as_sink(zend_object *val, GUID *sinkid, HashTable *id_to_name); +PHP_COM_DOTNET_API IDispatch *php_com_wrapper_export(zend_object *val); /* com_persist.c */ void php_com_persist_minit(INIT_FUNC_ARGS); diff --git a/ext/curl/tests/curl_headerfunction_throws_abort.phpt b/ext/curl/tests/curl_headerfunction_throws_abort.phpt index 9a69c966f144..23fe1652c636 100644 --- a/ext/curl/tests/curl_headerfunction_throws_abort.phpt +++ b/ext/curl/tests/curl_headerfunction_throws_abort.phpt @@ -25,7 +25,7 @@ curl_setopt($ch, CURLOPT_HEADERFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_WRITE_ERROR); @@ -39,7 +39,7 @@ var_dump(curl_errno($ch) === CURLE_OK); ?> --EXPECTF-- Test: header function throws exception -header exception +Exception: header exception bool(true) Test: header function is null bool(true) diff --git a/ext/curl/tests/curl_prereqfunction_throws_abort.phpt b/ext/curl/tests/curl_prereqfunction_throws_abort.phpt index 7e8ccbf94f53..9ba19c650c99 100644 --- a/ext/curl/tests/curl_prereqfunction_throws_abort.phpt +++ b/ext/curl/tests/curl_prereqfunction_throws_abort.phpt @@ -24,12 +24,12 @@ curl_setopt($ch, CURLOPT_PREREQFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK); ?> --EXPECTF-- -prereq exception +Exception: prereq exception bool(true) diff --git a/ext/curl/tests/curl_progressfunction_throws_abort.phpt b/ext/curl/tests/curl_progressfunction_throws_abort.phpt index 55e0f76cb61f..5d3599679834 100644 --- a/ext/curl/tests/curl_progressfunction_throws_abort.phpt +++ b/ext/curl/tests/curl_progressfunction_throws_abort.phpt @@ -26,7 +26,7 @@ curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK); @@ -40,7 +40,7 @@ var_dump(curl_errno($ch) === CURLE_OK); ?> --EXPECTF-- Test: progress function throws exception -info exception +Exception: info exception bool(true) Test: progress function is null bool(true) diff --git a/ext/curl/tests/curl_read_function_error_on_int.phpt b/ext/curl/tests/curl_read_function_error_on_int.phpt index 30ba97737727..0b4d86e07b43 100644 --- a/ext/curl/tests/curl_read_function_error_on_int.phpt +++ b/ext/curl/tests/curl_read_function_error_on_int.phpt @@ -21,10 +21,10 @@ curl_setopt($ch, CURLOPT_READFUNCTION, "custom_readfunction" ); try { curl_exec($ch); } catch (ValueError $e) { - echo $e->getMessage() . "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_error($ch)); ?> --EXPECT-- -The CURLOPT_READFUNCTION callback must return a string or CURL_READFUNC_ABORT or CURL_READFUNC_PAUSE +ValueError: The CURLOPT_READFUNCTION callback must return a string or CURL_READFUNC_ABORT or CURL_READFUNC_PAUSE string(29) "operation aborted by callback" diff --git a/ext/curl/tests/curl_readfunction_throws_abort.phpt b/ext/curl/tests/curl_readfunction_throws_abort.phpt index a030f8c4f41e..a18977d3ce45 100644 --- a/ext/curl/tests/curl_readfunction_throws_abort.phpt +++ b/ext/curl/tests/curl_readfunction_throws_abort.phpt @@ -28,7 +28,7 @@ curl_setopt($ch, CURLOPT_READFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK); @@ -42,7 +42,7 @@ var_dump(curl_errno($ch) === CURLE_OK); ?> --EXPECTF-- Test: read function throws exception -read exception +Exception: read exception bool(true) Test: read function is null bool(true) diff --git a/ext/curl/tests/curl_writefunction_throws_abort.phpt b/ext/curl/tests/curl_writefunction_throws_abort.phpt index 3da2fe8107b4..125961c6c666 100644 --- a/ext/curl/tests/curl_writefunction_throws_abort.phpt +++ b/ext/curl/tests/curl_writefunction_throws_abort.phpt @@ -25,7 +25,7 @@ curl_setopt($ch, CURLOPT_WRITEFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_WRITE_ERROR); @@ -38,7 +38,7 @@ var_dump(curl_errno($ch) === CURLE_OK); ?> --EXPECTF-- Test: write function throws exception -write exception +Exception: write exception bool(true) Test: write function is null Hello World! diff --git a/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt b/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt index fbc28f07ee96..28cab577db57 100644 --- a/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt +++ b/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt @@ -26,7 +26,7 @@ curl_setopt($ch, CURLOPT_XFERINFOFUNCTION, try { curl_exec($ch); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK); @@ -40,7 +40,7 @@ var_dump(curl_errno($ch) === CURLE_OK); ?> --EXPECTF-- Test: xfer info function throws exception -info exception +Exception: info exception bool(true) Test: xfer info function is null bool(true) diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 4b5f4d64c0ad..ff355768e26f 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -2988,7 +2988,7 @@ static void php_zip_get_stream(INTERNAL_FUNCTION_PARAMETERS, int type, bool acce PHP_ZIP_STAT_INDEX(intern, index, flags, sb); } - stream = php_stream_zip_open(intern, &sb, mode, flags STREAMS_CC); + stream = php_stream_zip_open(Z_ZIP_P(self), &sb, mode, flags STREAMS_CC); if (stream) { php_stream_to_zval(stream, return_value); } else { diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h index e734c4628f02..a10b1910f2ad 100644 --- a/ext/zip/php_zip.h +++ b/ext/zip/php_zip.h @@ -90,7 +90,7 @@ typedef struct _ze_zip_object { #define Z_ZIP_P(zv) php_zip_fetch_object(Z_OBJ_P((zv))) php_stream *php_stream_zip_opener(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC); -php_stream *php_stream_zip_open(struct zip *arch, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC); +php_stream *php_stream_zip_open(ze_zip_object *obj, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC); extern const php_stream_wrapper php_stream_zip_wrapper; diff --git a/ext/zip/tests/gh17787.phpt b/ext/zip/tests/gh17787.phpt new file mode 100644 index 000000000000..f82cfa1145bd --- /dev/null +++ b/ext/zip/tests/gh17787.phpt @@ -0,0 +1,80 @@ +--TEST-- +GH-17787 (ZipArchive stream stops reading early when the archive is freed while the stream is open) +--EXTENSIONS-- +zip +--FILE-- +open($name, ZipArchive::CREATE | ZipArchive::OVERWRITE); +$zip->addFromString('entry.txt', $data); +$zip->close(); + +$zip = new ZipArchive; +$zip->open($name, ZipArchive::RDONLY); +$stream = $zip->getStreamIndex(0, ZipArchive::FL_UNCHANGED); + +// Free the archive while the stream is still open +$zip = null; + +var_dump(stream_get_contents($stream) === $data); +fclose($stream); + +// Same with getStreamName() +$zip = new ZipArchive; +$zip->open($name, ZipArchive::RDONLY); +$stream = $zip->getStreamName('entry.txt', ZipArchive::FL_UNCHANGED); +$zip = null; + +var_dump(stream_get_contents($stream) === $data); +fclose($stream); + +// Same with getStream() +$zip = new ZipArchive; +$zip->open($name, ZipArchive::RDONLY); +$stream = $zip->getStream('entry.txt'); +$zip = null; + +var_dump(stream_get_contents($stream) === $data); +fclose($stream); + +// Pending changes are still committed once the last stream is closed +$name = __DIR__ . '/gh17787_write.zip'; + +$zip = new ZipArchive; +var_dump($zip->open($name, ZipArchive::CREATE | ZipArchive::OVERWRITE)); +$zip->addFromString('first.txt', 'first'); +$zip->close(); + +$zip = new ZipArchive; +var_dump($zip->open($name)); +$zip->addFromString('second.txt', 'second'); +$stream = $zip->getStreamName('first.txt', ZipArchive::FL_UNCHANGED); +$zip = null; + +var_dump(stream_get_contents($stream)); +fclose($stream); + +$zip = new ZipArchive; +var_dump($zip->open($name, ZipArchive::RDONLY)); +var_dump($zip->numFiles); +var_dump($zip->getFromName('second.txt')); +$zip->close(); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(5) "first" +bool(true) +int(2) +string(6) "second" diff --git a/ext/zip/zip_stream.c b/ext/zip/zip_stream.c index d4c24bd24e13..89c6a46e653d 100644 --- a/ext/zip/zip_stream.c +++ b/ext/zip/zip_stream.c @@ -32,6 +32,7 @@ struct php_zip_stream_data_t { struct zip_file *zf; size_t cursor; php_stream *stream; + ze_zip_object *owner; }; #define STREAM_DATA_FROM_STREAM() \ @@ -92,6 +93,12 @@ static int php_zip_ops_close(php_stream *stream, int close_handle) self->za = NULL; } } + + /* the pinned object ref is tied to self, so release it regardless of close_handle */ + if (self->owner) { + OBJ_RELEASE(&self->owner->zo); + self->owner = NULL; + } efree(self); stream->abstract = NULL; return EOF; @@ -227,8 +234,9 @@ const php_stream_ops php_stream_zipio_ops = { }; /* {{{ php_stream_zip_open */ -php_stream *php_stream_zip_open(struct zip *arch, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC) +php_stream *php_stream_zip_open(ze_zip_object *obj, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC) { + struct zip *arch = obj->za; struct zip_file *zf = NULL; php_stream *stream = NULL; @@ -247,6 +255,9 @@ php_stream *php_stream_zip_open(struct zip *arch, struct zip_stat *sb, const cha self->zf = zf; self->stream = NULL; self->cursor = 0; + /* keep the archive object alive while the stream borrows its zip_t */ + self->owner = obj; + GC_ADDREF(&obj->zo); #if LIBZIP_ATLEAST(1,9,1) if (zip_file_is_seekable(zf) > 0) { stream = php_stream_alloc(&php_stream_zipio_seek_ops, self, NULL, mode); @@ -332,6 +343,7 @@ php_stream *php_stream_zip_opener(php_stream_wrapper *wrapper, self->zf = zf; self->stream = NULL; self->cursor = 0; + self->owner = NULL; #if LIBZIP_ATLEAST(1,9,1) if (zip_file_is_seekable(zf) > 0) { stream = php_stream_alloc(&php_stream_zipio_seek_ops, self, NULL, mode); diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc index 3ad6ced5cb44..feee2bbb5686 100644 --- a/sapi/cli/tests/php_cli_server.inc +++ b/sapi/cli/tests/php_cli_server.inc @@ -16,8 +16,8 @@ function php_cli_server_start( $php_executable = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; $error = null; - // Create dedicated doc root to avoid index.php clashes between tests. - $doc_root = __DIR__ . DIRECTORY_SEPARATOR . basename($_SERVER['PHP_SELF'], '.php'); + // Create dedicated doc root to avoid index.php clashes between processes. + $doc_root = __DIR__ . DIRECTORY_SEPARATOR . basename($_SERVER['PHP_SELF'], '.php') . '-' . getmypid(); @mkdir($doc_root); if ($code) { diff --git a/sapi/cli/tests/php_cli_server_002.phpt b/sapi/cli/tests/php_cli_server_002.phpt index d2b561b8bb6b..e3204c06ee8d 100644 --- a/sapi/cli/tests/php_cli_server_002.phpt +++ b/sapi/cli/tests/php_cli_server_002.phpt @@ -13,7 +13,7 @@ php_cli_server_start('var_dump($_SERVER["DOCUMENT_ROOT"], $_SERVER["SERVER_SOFTW var_dump(file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS)); ?> --EXPECTF-- -string(%d) "string(%d) "%sphp_cli_server_002" +string(%d) "string(%d) "%sphp_cli_server_002-%d" string(%d) "PHP/%s (Development Server)" string(%d) "localhost" string(%d) "%s"