Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the XML component
ComponentDOM
Bug ClassUAF
Tracker2076705
Fix commite07cffdbb7f5 (firefox) +1/-1
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`nsXMLContentSink`
the content sink that builds the DOM tree while parsing an XML document; CloseElement is called when the parser finishes an element and is where post-processing such as DoneAddingChildren() and script/stylesheet handling happens.
`nsIContent` / `mozilla::dom::NodeInfo`
each DOM node holds a NodeInfo (name/namespace/document metadata). NodeInfo is 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.

Key insight
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

  1. Craft XML Serve an XML (or XHTML) document whose parsing drives nsXMLContentSink::CloseElement on a chosen element. (web-reachable: XML parsing is content-triggered.)
  2. Close an element that runs callbacks Target an element whose CloseElement path invokes DoneAddingChildren()/script that can mutate the tree (e.g. a scripted or form-state element).
  3. Drop the node mid-close Use that script/side effect to remove the just-closed element from the document, releasing the last reference to aContent and destroying its NodeInfo.
  4. Stale use CloseElement continues and dereferences the now-dangling nodeInfo raw pointer.
  5. Primitive Use-after-free of a NodeInfo object in the content process, exploitable via heap grooming for controlled memory corruption. (escalation inferred)

Impact Assessment

The bug is in the renderer/content process XML content sink and is reachable by loading attacker-controlled XML/XHTML. The attacker must arrange for a mutation (typically via script run during element close) that frees the element whose 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 callbacks
    grep the content sinks (nsHTMLContentSink, fragment sinks) and DOM code for NodeInfo* 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 paths
    look for other places parsing can synchronously execute script (XSLT, inline scripts, form restoration) while a raw node/NodeInfo pointer is live.
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).
Loading diff…