CVE-2026-117974
Overview
Background
- `nsXMLContentSink`
- the content sink that builds the DOM tree while parsing an XML document;
CloseElementis called when the parser finishes an element and is where post-processing such asDoneAddingChildren()and script/stylesheet handling happens. - `nsIContent` / `mozilla::dom::NodeInfo`
- each DOM node holds a
NodeInfo(name/namespace/document metadata).NodeInfois reference-counted (NS_INLINE_DECL_...REFCOUNTING);nsIContent::NodeInfo()returns a borrowed raw pointer whose lifetime is tied to the owning node. - `DoneAddingChildren()` / script execution during sink callbacks
- closing an element can run script, flush the parser, or otherwise mutate/remove nodes, potentially dropping the last reference to the element just closed and to its
NodeInfo.
Root Cause Analysis
nsXMLContentSink::CloseElement captured the element’s NodeInfo into a raw, non-owning pointer: mozilla::dom::NodeInfo* nodeInfo = aContent->NodeInfo();. It then performs further work in the same function — notably calling DoneAddingChildren() and handling HTML elements that can execute script or run synchronous callbacks — before using nodeInfo again. Those intermediate operations can tear down the DOM subtree, drop the last strong reference to aContent, and thereby destroy its NodeInfo. Any subsequent dereference of the stale nodeInfo raw pointer in CloseElement is a use-after-free. The violated invariant is that a raw NodeInfo* obtained from NodeInfo() is only valid as long as the owning node is kept alive, which CloseElement did not guarantee across its own re-entrant callbacks.
The one-line patch changes the capture to an owning reference: RefPtr<mozilla::dom::NodeInfo> nodeInfo = aContent->NodeInfo();. The RefPtr takes a strong reference, so the NodeInfo survives even if aContent and the surrounding subtree are destroyed by script or DoneAddingChildren() side effects during CloseElement, eliminating the dangling access.
CloseElement held the element’s NodeInfo as a borrowed raw pointer across re-entrant calls (DoneAddingChildren, script) that can free the node, so a later use dereferenced freed memory; taking a RefPtr keeps the NodeInfo alive for the duration.Attack Path
- Craft XML
Serve an XML (or XHTML) document whose parsing drives
nsXMLContentSink::CloseElementon a chosen element. (web-reachable: XML parsing is content-triggered.) - Close an element that runs callbacks
Target an element whose
CloseElementpath invokesDoneAddingChildren()/script that can mutate the tree (e.g. a scripted or form-state element). - Drop the node mid-close
Use that script/side effect to remove the just-closed element from the document, releasing the last reference to
aContentand destroying itsNodeInfo. - Stale use
CloseElementcontinues and dereferences the now-danglingnodeInforaw pointer. - Primitive
Use-after-free of a
NodeInfoobject in the content process, exploitable via heap grooming for controlled memory corruption. (escalation inferred)
Impact Assessment
NodeInfo was captured — a plausible, content-controllable precondition rather than a race. The primitive is a use-after-free of a reference-counted NodeInfo; the advisory’s “use-after-free in the XML component” matches the diff. Escalation from the UAF to code execution is inferred and not demonstrated by the diff.Files Changed
dom/xml/nsXMLContentSink.cpp
Audit Directions
- Raw `NodeInfo()` / `OwnerDoc()` captures across callbacksgrep the content sinks (
nsHTMLContentSink, fragment sinks) and DOM code forNodeInfo* x = ...->NodeInfo()(or similar borrowed pointers) held across calls that can run script or mutate the tree. - Re-entrancy via `DoneAddingChildren`review all callers of
DoneAddingChildren()for borrowed pointers to the node or its members that outlive the call. - Borrowed pointers in parser pathslook for other places parsing can synchronously execute script (XSLT, inline scripts, form restoration) while a raw node/NodeInfo pointer is live.
Patch
diff --git a/dom/xml/nsXMLContentSink.cpp b/dom/xml/nsXMLContentSink.cpp
index a81edfc6cb7..073827fdfa1 100644
--- a/dom/xml/nsXMLContentSink.cpp
+++ b/dom/xml/nsXMLContentSink.cpp
@@ -596,7 +596,7 @@ nsresult nsXMLContentSink::CreateElement(
nsresult nsXMLContentSink::CloseElement(nsIContent* aContent) {
NS_ASSERTION(aContent, "missing element to close");
- mozilla::dom::NodeInfo* nodeInfo = aContent->NodeInfo();
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo = aContent->NodeInfo();
// Some HTML nodes need DoneAddingChildren() called to initialize
// properly (eg form state restoration).