From 32d09d095053686cfa92e7dbdc04271a3ba0dfb3 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 31 Oct 2026 11:28:45 -0400 Subject: [PATCH] Release the parent's reference in MimeMessage::remove() php_mimepart_remove_from_parent() deleted the child's entry from the parent's children table, which has no destructor, without dropping the reference that entry held. A removed part therefore stayed allocated until the end of the request, so a loop that builds a message and removes a part grows memory without bound in a long-running process. --- CHANGELOG.md | 1 + php_mailparse_mime.c | 4 ++ tests/mimemessage_remove_releases_part.phpt | 41 +++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/mimemessage_remove_releases_part.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d97044..fe8a6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix memory leaks - Fix `MimeMessage::extract_uue()` ignoring index greater than zero - Fix use-after-free when a MIME part outlives its parent, or when a child part is freed before its parent. `mailparse_msg_free()` on a part that still belongs to a parent now returns true without freeing it; the part is freed with its root. (Firas Fatnassi, Oz Kaiser) +- Fix `MimeMessage::remove()` keeping the removed part alive until the end of the request. # Version 3.2.0 - 2026-04-09 diff --git a/php_mailparse_mime.c b/php_mailparse_mime.c index b8a5e5a..212a43e 100644 --- a/php_mailparse_mime.c +++ b/php_mailparse_mime.c @@ -1026,8 +1026,12 @@ PHP_MAILPARSE_API void php_mimepart_remove_from_parent(php_mimepart *part) mailparse_fetch_mimepart_resource(childpart, childpart_z); if (childpart == part) { zend_ulong h; + zval child_z; + + ZVAL_COPY_VALUE(&child_z, childpart_z); zend_hash_get_current_key_ex(&parent->children, NULL, &h, &pos); zend_hash_index_del(&parent->children, h); + zval_ptr_dtor(&child_z); break; } } diff --git a/tests/mimemessage_remove_releases_part.phpt b/tests/mimemessage_remove_releases_part.phpt new file mode 100644 index 0000000..fe23df9 --- /dev/null +++ b/tests/mimemessage_remove_releases_part.phpt @@ -0,0 +1,41 @@ +--TEST-- +MimeMessage::remove() releases the removed part +--SKIPIF-- + +--FILE-- +get_child_count()); +$child = $root->get_child(1); +$child->remove(); +var_dump($root->get_child_count()); +var_dump($child->get_parent()); +var_dump($root->get_child(1)->data['content-type']); +unset($child, $root); + +$warmup = new MimeMessage('var', $data); +$warmup->get_child(1)->remove(); +unset($warmup); + +$before = memory_get_usage(); +for ($i = 0; $i < 100; $i++) { + $msg = new MimeMessage('var', $data); + $msg->get_child(1)->remove(); + unset($msg); +} +var_dump(memory_get_usage() - $before); +?> +--EXPECT-- +int(3) +int(2) +NULL +string(9) "text/html" +int(0)