Skip to content

Fix use-after-free between MIME parts with mismatched lifetimes - #75

Closed
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix-mimepart-lifetime
Closed

iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix-mimepart-lifetime

Conversation

@iliaal

@iliaal iliaal commented Oct 31, 2026

Copy link
Copy Markdown
Member

This fixes a use-after-free reported privately by Firas Fatnassi and Oz Kaiser. Each part keeps a raw parent pointer, and each parent a raw parsedata.lastpart pointer to its last child. Since bd4a675 dropped the destructor that detached children, a child kept alive by mailparse_msg_get_part() or MimeMessage::get_child() outlives its root with a dangling parent pointer, which get_parent() reads and further mailparse_msg_parse() calls on the child write through. Freeing in the other order was broken before 3.1.5 too: mailparse_msg_free() on a child closed it while the parent still pointed at it, and MimeMessage::remove() leaves the same dangling lastpart.

php_mimepart_free() now detaches surviving children (Oz Kaiser's patch, together with two of the tests), remove() clears the parent's lastpart, and mailparse_msg_free() on a part that still belongs to a parent returns true without freeing it; the part is freed with its root. Code that frees each part and then the root keeps working, but a child's memory is now held until its root is freed. A child that outlives its parent reports top-level offsets, as it did before 3.1.5. Checked with ASan on PHP 8.4 under both allocators, and against the pre-bd4a675 tree to confirm where the regression came in.

A part keeps raw pointers to its parent (part->parent) and to its last
child (parsedata.lastpart). Since bd4a675 dropped the detaching child
destructor, freeing a root while userland still holds a child leaves
child->parent dangling, and MimeMessage::get_parent(), further
mailparse_msg_parse() calls on the child, and position updates read or
write the freed parent. Freeing in the other order is also unsafe:
mailparse_msg_free() on a child force-closes it, leaving the parent's
lastpart dangling and a closed resource in its children table, so
continued parsing hits freed memory and mailparse_msg_get_structure()
dereferences NULL. MimeMessage::remove() leaves the same dangling
lastpart once the removed part is freed.

php_mimepart_free() now clears the parent pointer of every surviving
child, php_mimepart_remove_from_parent() clears the parent's lastpart,
and mailparse_msg_free() returns true without freeing a part that still
has a parent; that part is freed with its root. A child that outlives
its parent is reported as a top-level part, as it was before bd4a675.

Reported by Firas Fatnassi and Oz Kaiser. The child-detach change and the
two outlives-parent tests are based on Oz Kaiser's patch.
@remicollet

Copy link
Copy Markdown
Member

Merged, thanks

@remicollet remicollet closed this Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants