[PECL-CVS] [pecl-mail-mailparse] master: Merge pull request #46 from php/fix/gh44-segfault-mimepart-dtor

[email protected] (Rasmus Lerdorf via GitHub) Sun, 5 Apr 2026 11:51:09 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Committer: GitHub (web-flow)
Pusher: rlerdorf
Date: 2026-04-05T12:51:06+01:00

Commit: https://github.com/php/pecl-mail-mailparse/commit/84384b16c53c570c20b0b0c424a6a983d2378502
Raw diff: https://github.com/php/pecl-mail-mailparse/commit/84384b16c53c570c20b0b0c424a6a983d2378502.diff

Merge pull request #46 from php/fix/gh44-segfault-mimepart-dtor

potential fix for #44

Changed paths:
  A  .gitattributes
  A  tests/gh44.phpt
  M  .github/workflows/ci.yml
  M  mailparse.c
  M  php_mailparse_mime.c


Diff:

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..bd8af52
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Keep test files and test data with LF line endings on all platforms
+# to ensure byte offsets and string lengths match expected values.
+*.phpt text eol=lf
+tests/testdata/* -text
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c6de5e2..431ae7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,13 +4,13 @@ jobs:
   ubuntu:
     strategy:
       matrix:
-          version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
+          version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
     runs-on: ubuntu-latest
     steps:
       - name: Install re2c
         run: sudo apt-get install -y re2c
       - name: Checkout mailparse
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
       - name: Setup PHP
         uses: shivammathur/setup-php@v2
         with:
@@ -34,19 +34,18 @@ jobs:
       run:
         shell: cmd
     strategy:
+      fail-fast: false
       matrix:
-          version: ["7.4", "8.0"]
+          version: ["8.1", "8.2", "8.3", "8.4", "8.5"]
           arch: [x64, x86]
           ts: [ts]
     runs-on: windows-latest
     steps:
-      - name: Configure Git
-        run: git config --global core.autocrlf false
       - name: Checkout mailparse
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
       - name: Setup PHP
         id: setup-php
-        uses: cmb69/[email protected]
+        uses: php/setup-php-sdk@fix/invoke-webrequest-tls
         with:
           version: ${{matrix.version}}
           arch: ${{matrix.arch}}
@@ -63,4 +62,6 @@ jobs:
       - name: make
         run: nmake
       - name: test
-        run: nmake test TESTS="-d extension=${{steps.setup-php.outputs.prefix}}\ext\php_mbstring.dll --show-diff tests"
+        run: |
+          set PATH=%PATH%;${{steps.setup-php.outputs.prefix}}\ext
+          nmake test TESTS="-d extension=${{steps.setup-php.outputs.prefix}}\ext\php_mbstring.dll --show-diff tests"
diff --git a/mailparse.c b/mailparse.c
index ba65418..dba3309 100644
--- a/mailparse.c
+++ b/mailparse.c
@@ -110,7 +110,10 @@ ZEND_RSRC_DTOR_FUNC(mimepart_dtor)
 {
 	php_mimepart *part = res->ptr;
 
-	php_mimepart_free(part);
+	if (part != NULL) {
+		res->ptr = NULL;
+		php_mimepart_free(part);
+	}
 }
 
 PHP_INI_BEGIN()
diff --git a/php_mailparse_mime.c b/php_mailparse_mime.c
index afa28bd..e71bfec 100644
--- a/php_mailparse_mime.c
+++ b/php_mailparse_mime.c
@@ -321,7 +321,16 @@ PHP_MAILPARSE_API void php_mimepart_free(php_mimepart *part)
 	zval *childpart_z;
 	HashPosition pos;
 
-	/* free contained parts */
+	/* Prevent the resource destructor from freeing this part again */
+	if (part->rsrc && part->rsrc->ptr == part) {
+		part->rsrc->ptr = NULL;
+	}
+
+	/* Release child resources via zval_ptr_dtor, which respects refcounts:
+	 * children with no external references are freed immediately, while
+	 * children held by userland (e.g. via mailparse_msg_get_part) stay
+	 * alive until their own refcount reaches zero. The idempotent
+	 * mimepart_dtor (res->ptr NULL guard) prevents double-free at shutdown. */
 	zend_hash_internal_pointer_reset_ex(&part->children, &pos);
 	while ((childpart_z = zend_hash_get_current_data_ex(&part->children, &pos)) != NULL) {
 		zval_ptr_dtor(childpart_z);
diff --git a/tests/gh44.phpt b/tests/gh44.phpt
new file mode 100644
index 0000000..d8f185d
--- /dev/null
+++ b/tests/gh44.phpt
@@ -0,0 +1,56 @@
+--TEST--
+GH issue #44 (Segmentation fault in mimepart resource destructor during shutdown)
+--SKIPIF--
+<?php
+if (!extension_loaded("mailparse")) die("skip mailparse extension not available");
+?>
+--FILE--
+<?php
+/* Generate a multipart message with >300 parts to trigger MAXPARTS.
+ * This exercises the mailparse_msg_parse_file error path which calls
+ * php_mimepart_free() directly, leaving child resources in the resource
+ * list. Without the fix, these dangling resources cause a use-after-free
+ * during shutdown in zend_close_rsrc_list. */
+
+$boundary = "test_boundary";
+$mime = "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n\r\n";
+for ($i = 0; $i < 302; $i++) {
+    $mime .= "--$boundary\r\nContent-Type: text/plain\r\n\r\npart $i\r\n";
+}
+$mime .= "--$boundary--\r\n";
+
+$tmpfile = tempnam(sys_get_temp_dir(), 'mp_gh44_');
+file_put_contents($tmpfile, $mime);
+
+/* Parse the oversized message - triggers "MIME message too complex" warning
+ * and the error path calls php_mimepart_free(part) directly, freeing the
+ * mimepart struct but leaving child zend_resource entries in the list */
+$result = @mailparse_msg_parse_file($tmpfile);
+echo "parse_file returned: " . var_export($result, true) . "\n";
+
+/* Allocate many messages to reuse freed memory from the failed parse above.
+ * This increases the chance that the dangling resource pointers from the
+ * failed parse will reference reused/corrupted memory at shutdown. */
+$msgs = [];
+for ($i = 0; $i < 50; $i++) {
+    $m = mailparse_msg_create();
+    mailparse_msg_parse($m, "Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" .
+        "--b\r\nContent-Type: text/plain\r\n\r\nhello\r\n" .
+        "--b\r\nContent-Type: text/html\r\n\r\n<b>hi</b>\r\n--b--\r\n");
+    $msgs[] = $m;
+}
+
+/* Also test normal multipart parsing and cleanup */
+$msg = mailparse_msg_create();
+mailparse_msg_parse($msg, "Content-Type: multipart/mixed; boundary=\"x\"\r\n\r\n" .
+    "--x\r\nContent-Type: text/plain\r\n\r\nhello\r\n--x--\r\n");
+$struct = mailparse_msg_get_structure($msg);
+echo "structure: " . implode(", ", $struct) . "\n";
+
+unlink($tmpfile);
+echo "ok\n";
+?>
+--EXPECT--
+parse_file returned: false
+structure: 1, 1.1
+ok