Commit 0614b703d85 for php

commit 0614b703d85fe2cd1debb7f04ce8a31682460893
Author: David Carlier <devnexen@gmail.com>
Date:   Mon Nov 2 22:11:18 2026 +0000

    Fix GH-25118: Dom\HTMLDocument::createFromString() crash on OOM

    The bridge did not handle failed lexbor allocations, and on error freed
    template fragments after their document.

    Close GH-25124

diff --git a/NEWS b/NEWS
index 556548f2c0b..77324231844 100644
--- a/NEWS
+++ b/NEWS
@@ -66,6 +66,8 @@ PHP                                                                        NEWS
   . Fixed a use-after-free when reflected attribute setters replace an
     attribute whose child still has a live wrapper. (Jakub Zelenka)
   . Fix error paths in dom_xml_serializing_a_document_node(). (ndossche)
+  . Fixed bug GH-25118 (Dom\HTMLDocument::createFromString() crash on OOM).
+    (devnexen)

 - FFI:
   . Fixed bug GH-24582 (Memory leaks in ext/ffi parser when finalize_type()
diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c
index d946979b705..74c76cb95f6 100644
--- a/ext/dom/html5_parser.c
+++ b/ext/dom/html5_parser.c
@@ -42,7 +42,7 @@ typedef struct work_list_item {
     bool in_template;
 } work_list_item;

-static void lexbor_libxml2_bridge_work_list_item_push(
+static bool lexbor_libxml2_bridge_work_list_item_push(
     lexbor_array_obj_t *array,
     lxb_dom_node_t *node,
     uintptr_t current_active_namespace,
@@ -52,11 +52,15 @@ static void lexbor_libxml2_bridge_work_list_item_push(
 )
 {
     work_list_item *item = (work_list_item *) lexbor_array_obj_push_wo_cls(array);
+    if (UNEXPECTED(item == NULL)) {
+        return false;
+    }
     item->node = node;
     item->current_active_namespace = current_active_namespace;
     item->lxml_parent = lxml_parent;
     item->lxml_ns = lxml_ns;
     item->in_template = in_template;
+    return true;
 }

 static unsigned short sanitize_line_nr(size_t line)
@@ -125,10 +129,15 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert(
     xmlNsPtr prefixed_xmlns_ns = NULL;

     lexbor_array_obj_t work_list;
-    lexbor_array_obj_init(&work_list, WORK_LIST_INIT_SIZE, sizeof(work_list_item));
+    if (UNEXPECTED(lexbor_array_obj_init(&work_list, WORK_LIST_INIT_SIZE, sizeof(work_list_item)) != LXB_STATUS_OK)) {
+        return LEXBOR_LIBXML2_BRIDGE_STATUS_OOM;
+    }

     for (lxb_dom_node_t *node = start_node; node != NULL; node = node->prev) {
-        lexbor_libxml2_bridge_work_list_item_push(&work_list, node, LXB_NS__UNDEF, root, NULL, in_template_contents);
+        if (UNEXPECTED(!lexbor_libxml2_bridge_work_list_item_push(&work_list, node, LXB_NS__UNDEF, root, NULL, in_template_contents))) {
+            lexbor_array_obj_destroy(&work_list, false);
+            return LEXBOR_LIBXML2_BRIDGE_STATUS_OOM;
+        }
     }

     work_list_item *current_stack_item;
@@ -200,14 +209,20 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert(
 			}

             for (; child_node != NULL; child_node = child_node->prev) {
-                lexbor_libxml2_bridge_work_list_item_push(
+                if (UNEXPECTED(!lexbor_libxml2_bridge_work_list_item_push(
                     &work_list,
                     child_node,
                     entering_namespace,
                     lxml_child_parent,
                     current_lxml_ns,
                     child_in_template
-                );
+                ))) {
+                    retval = LEXBOR_LIBXML2_BRIDGE_STATUS_OOM;
+                    break;
+                }
+            }
+            if (UNEXPECTED(retval != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) {
+                break;
             }

             xmlAttrPtr last_added_attr = NULL;
@@ -381,6 +396,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_document(
         private_data
     );
     if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) {
+        php_dom_remove_templated_content_in_subtree(private_data, (xmlNodePtr) lxml_doc);
         xmlFreeDoc(lxml_doc);
         return status;
     }
@@ -412,6 +428,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment(
         private_data
     );
     if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) {
+        php_dom_remove_templated_content_in_subtree(private_data, fragment);
         xmlFreeNode(fragment);
         return status;
     }
diff --git a/ext/dom/private_data.c b/ext/dom/private_data.c
index 66b3f4f87f2..213eff8a61b 100644
--- a/ext/dom/private_data.c
+++ b/ext/dom/private_data.c
@@ -70,8 +70,12 @@ void php_dom_private_data_destroy(php_dom_private_data *data)
 	efree(data);
 }

-static void php_dom_free_templated_content(php_dom_private_data *private_data, xmlNodePtr base)
+void php_dom_remove_templated_content_in_subtree(php_dom_private_data *private_data, xmlNodePtr base)
 {
+	if (private_data->template_fragments == NULL) {
+		return;
+	}
+
 	/* Note: it's not possible to obtain a userland reference to these yet, so we can just free them without worrying
 	 *       about their proxies.
 	 * Note 2: it's possible to have nested template content. */
@@ -87,7 +91,11 @@ static void php_dom_free_templated_content(php_dom_private_data *private_data, x
 			current = php_dom_next_in_tree_order(current, base);
 		}
 	}
+}

+static void php_dom_free_templated_content(php_dom_private_data *private_data, xmlNodePtr base)
+{
+	php_dom_remove_templated_content_in_subtree(private_data, base);
 	xmlFreeNode(base);
 }

diff --git a/ext/dom/private_data.h b/ext/dom/private_data.h
index 109f06c9050..df6cf407cf6 100644
--- a/ext/dom/private_data.h
+++ b/ext/dom/private_data.h
@@ -50,6 +50,7 @@ void php_dom_add_templated_content(php_dom_private_data *private_data, const xml
 xmlNodePtr php_dom_retrieve_templated_content(php_dom_private_data *private_data, const xmlNode *template_node);
 xmlNodePtr php_dom_ensure_templated_content(php_dom_private_data *private_data, xmlNodePtr template_node);
 void php_dom_remove_templated_content(php_dom_private_data *private_data, const xmlNode *template_node);
+void php_dom_remove_templated_content_in_subtree(php_dom_private_data *private_data, xmlNodePtr base);
 uint32_t php_dom_get_template_count(const php_dom_private_data *private_data);
 void dom_add_element_ns_hook(php_dom_private_data *private_data, xmlNodePtr element);

diff --git a/ext/dom/tests/gh25118.phpt b/ext/dom/tests/gh25118.phpt
new file mode 100644
index 00000000000..790ae71ef33
--- /dev/null
+++ b/ext/dom/tests/gh25118.phpt
@@ -0,0 +1,17 @@
+--TEST--
+GH-25118 (Dom\HTMLDocument::createFromString() crashes on OOM during conversion)
+--EXTENSIONS--
+dom
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip 64-bit only");
+?>
+--INI--
+memory_limit=32M
+--FILE--
+<?php
+$html = '<template><p>t</p><template><b>n</b></template></template><main>' . str_repeat('<i></i>', 100000) . '</main>';
+Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
+?>
+--EXPECTF--
+Fatal error: Allowed memory size of 33554432 bytes exhausted %S(tried to allocate %d bytes) in %s on line %d