From 5be9d0052548abfc90e897b683c46492167d1c87 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 18 Apr 2026 09:11:37 -0400 Subject: [PATCH 1/3] phar: cap OpenSSL signature length to prevent oversized allocation A uint32_t signature length read from the phar file was passed directly to emalloc() without any upper bound. On a 64-bit build a crafted phar could request a multi-gigabyte heap allocation before any file I/O confirmed the data existed. Cap signature_len at 1 MiB. Real RSA signatures are at most ~512 bytes for 4096-bit keys; 1 MiB is generous enough to avoid false positives while blocking resource exhaustion. Closes GH-21798 --- ext/phar/phar.c | 8 +++++ ext/phar/tests/gh21798-sig-len-cap.phpt | 39 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 ext/phar/tests/gh21798-sig-len-cap.phpt diff --git a/ext/phar/phar.c b/ext/phar/phar.c index 7e74de782ccb..204740f2b7b8 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -886,6 +886,14 @@ static zend_result phar_parse_pharfile(php_stream *fp, char *fname, size_t fname sig_ptr = sig_buf; PHAR_GET_32(sig_ptr, signature_len); + if (signature_len > 1024 * 1024) { + efree(savebuf); + php_stream_close(fp); + if (error) { + spprintf(error, 0, "phar \"%s\" openssl signature length is too large", fname); + } + return FAILURE; + } sig = (char *) emalloc(signature_len); whence = signature_len + 4; whence = -whence; diff --git a/ext/phar/tests/gh21798-sig-len-cap.phpt b/ext/phar/tests/gh21798-sig-len-cap.phpt new file mode 100644 index 000000000000..1746b7974d1d --- /dev/null +++ b/ext/phar/tests/gh21798-sig-len-cap.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-21798: phar rejects oversized OpenSSL signature length field +--EXTENSIONS-- +phar +--SKIPIF-- + +--INI-- +phar.require_hash=0 +phar.readonly=0 +--FILE-- +getMessage() . "\n"; +} +echo "done\n"; +?> +--CLEAN-- + +--EXPECTF-- +phar "%s" openssl signature length is too large +done From 766d6053d9900d556873f53a782b74f327491617 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 18 Apr 2026 09:17:43 -0400 Subject: [PATCH 2/3] phar: free is_temp_dir entry before rejecting .phar/* paths in offsetGet Phar::offsetGet() calls phar_get_entry_info_dir with allow_dir=1, which may return a heap-allocated temporary directory entry (is_temp_dir=1) for paths that resolve to a virtual directory in the manifest. Three early-exit paths for .phar/stub.php, .phar/alias.txt, and the generic .phar/* prefix all called RETURN_THROWS() before the is_temp_dir cleanup block, leaking the entry and its filename buffer on every rejection. Move the is_temp_dir cleanup before the .phar/* guards so all exit paths release the temporary entry regardless of which rejection fires. Closes GH-21798 --- ext/phar/phar_object.c | 10 ++--- .../tests/gh21798-offsetget-temp-entry.phpt | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 ext/phar/tests/gh21798-offsetget-temp-entry.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 18db3190bb03..2cb21b35fc26 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3591,6 +3591,11 @@ PHP_METHOD(Phar, offsetGet) if (!(entry = phar_get_entry_info_dir(phar_obj->archive, ZSTR_VAL(file_name), ZSTR_LEN(file_name), 1, &error, 0))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s does not exist%s%s", ZSTR_VAL(file_name), error?", ":"", error?error:""); } else { + if (entry->is_temp_dir) { + efree(entry->filename); + efree(entry); + } + if (zend_string_equals_literal(file_name, ".phar/stub.php")) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot get stub \".phar/stub.php\" directly in phar \"%s\", use getStub", phar_obj->archive->fname); RETURN_THROWS(); @@ -3606,11 +3611,6 @@ PHP_METHOD(Phar, offsetGet) RETURN_THROWS(); } - if (entry->is_temp_dir) { - efree(entry->filename); - efree(entry); - } - zend_string *sfname = strpprintf(0, "phar://%s/%s", phar_obj->archive->fname, ZSTR_VAL(file_name)); zval zfname; ZVAL_NEW_STR(&zfname, sfname); diff --git a/ext/phar/tests/gh21798-offsetget-temp-entry.phpt b/ext/phar/tests/gh21798-offsetget-temp-entry.phpt new file mode 100644 index 000000000000..b5b11ad43634 --- /dev/null +++ b/ext/phar/tests/gh21798-offsetget-temp-entry.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-21798: Phar::offsetGet() must free is_temp_dir entry before rejecting .phar/* paths +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--FILE-- +addFromString('index.php', ''); +unset($phar); + +$phar = new Phar($fname); +try { + $phar->offsetGet('.phar/stub.php'); +} catch (BadMethodCallException $e) { + echo $e->getMessage() . "\n"; +} +try { + $phar->offsetGet('.phar/alias.txt'); +} catch (BadMethodCallException $e) { + echo $e->getMessage() . "\n"; +} +try { + $phar->offsetGet('.phar/internal'); +} catch (BadMethodCallException $e) { + echo $e->getMessage() . "\n"; +} +echo "no crash\n"; +?> +--CLEAN-- + +--EXPECT-- +Entry .phar/stub.php does not exist +Entry .phar/alias.txt does not exist +Entry .phar/internal does not exist +no crash From da4a1d085a9d87566d3639c7df46660c76165604 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 18 Apr 2026 09:17:51 -0400 Subject: [PATCH 3/3] phar: call phar_entry_delref before goto finish in phar_add_file error paths phar_add_file opens or creates an entry via phar_get_or_create_entry_data_rw, which increments the entry's reference count and must be balanced by a phar_entry_delref call. Two error paths inside the content-write block jumped to finish: with goto, skipping the phar_entry_delref at line 3714. The finish: label comes after the delref, so both paths leaked the entry reference. Add phar_entry_delref(data) before each goto finish in the short-write and missing-resource branches. Closes GH-21798 --- ext/phar/phar_object.c | 2 ++ ext/phar/tests/gh21798-add-file-delref.phpt | 29 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 ext/phar/tests/gh21798-add-file-delref.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 2cb21b35fc26..7b675e2c900a 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3681,11 +3681,13 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con size_t written_len = php_stream_write(data->fp, ZSTR_VAL(content), ZSTR_LEN(content)); if (written_len != contents_len) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s could not be written to", filename); + phar_entry_delref(data); goto finish; } } else { if (!(php_stream_from_zval_no_verify(contents_file, zresource))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s could not be written to", filename); + phar_entry_delref(data); goto finish; } php_stream_copy_to_stream_ex(contents_file, data->fp, PHP_STREAM_COPY_ALL, &contents_len); diff --git a/ext/phar/tests/gh21798-add-file-delref.phpt b/ext/phar/tests/gh21798-add-file-delref.phpt new file mode 100644 index 000000000000..fa893a1975e3 --- /dev/null +++ b/ext/phar/tests/gh21798-add-file-delref.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-21798: phar_add_file must call phar_entry_delref on write error paths +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--FILE-- +addFromString('hello.txt', 'hello world'); +$phar->addFromString('empty.txt', ''); +unset($phar); + +$phar = new Phar($fname); +echo $phar['hello.txt']->getContent() . "\n"; +echo ($phar->offsetExists('empty.txt') ? 'empty exists' : 'missing') . "\n"; +echo "no crash\n"; +?> +--CLEAN-- + +--EXPECT-- +hello world +empty exists +no crash