[PECL-CVS] [pecl-mail-mailparse] fix/gh44-segfault-mimepart-dtor: Get the test closer to the actual crash env and fix Windows CI

[email protected] (Rasmus Lerdorf) Sun, 5 Apr 2026 11:05:28 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-05T07:05:22-04:00

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

Get the test closer to the actual crash env and fix Windows CI

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


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 6c94734..431ae7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,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/tests/gh44.phpt b/tests/gh44.phpt
index 669d86d..d8f185d 100644
--- a/tests/gh44.phpt
+++ b/tests/gh44.phpt
@@ -6,54 +6,51 @@ if (!extension_loaded("mailparse")) die("skip mailparse extension not available"
 ?>
 --FILE--
 <?php
-$mime = "Content-Type: multipart/mixed; boundary=\"outer\"\r\n" .
-    "\r\n" .
-    "--outer\r\n" .
-    "Content-Type: multipart/alternative; boundary=\"inner\"\r\n" .
-    "\r\n" .
-    "--inner\r\n" .
-    "Content-Type: text/plain\r\n" .
-    "\r\n" .
-    "Hello plain\r\n" .
-    "--inner\r\n" .
-    "Content-Type: text/html\r\n" .
-    "\r\n" .
-    "<b>Hello html</b>\r\n" .
-    "--inner--\r\n" .
-    "--outer\r\n" .
-    "Content-Type: application/octet-stream\r\n" .
-    "Content-Transfer-Encoding: base64\r\n" .
-    "\r\n" .
-    "SGVsbG8=\r\n" .
-    "--outer--\r\n";
+/* 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. */
 
-/* Test 1: Parse multipart message, let resource cleanup happen at shutdown */
-$msg1 = mailparse_msg_create();
-mailparse_msg_parse($msg1, $mime);
-$struct = mailparse_msg_get_structure($msg1);
-echo "structure: " . implode(", ", $struct) . "\n";
+$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";
 
-/* Test 2: Parse and explicitly free */
-$msg2 = mailparse_msg_create();
-mailparse_msg_parse($msg2, $mime);
-mailparse_msg_free($msg2);
+$tmpfile = tempnam(sys_get_temp_dir(), 'mp_gh44_');
+file_put_contents($tmpfile, $mime);
 
-/* Test 3: Get child parts (adds refcount), then let shutdown clean up */
-$msg3 = mailparse_msg_create();
-mailparse_msg_parse($msg3, $mime);
-$parts = [];
-foreach (mailparse_msg_get_structure($msg3) as $part_id) {
-    $parts[] = mailparse_msg_get_part($msg3, $part_id);
-}
+/* 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";
 
-/* Test 4: Multiple messages with interleaved resource handles */
-for ($i = 0; $i < 5; $i++) {
+/* 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, $mime);
+    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--
-structure: 1, 1.1, 1.1.1, 1.1.2, 1.2
+parse_file returned: false
+structure: 1, 1.1
 ok