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