High CVSS 6.5 firefox UAF 🔧 Commit mapped

Overview

High
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionJavaScript code running while transforming a document with the XSLTProcessor could lead to a use-after-free.
ComponentDOM
Bug ClassUAF
Tracker1941002
Fix commitbf261e3396f4 (firefox) +26/-9
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
CISA KEVNot listed
CreditedIvan Fratric of Google Project Zero
Disclosed2025-04-01

Background

`mSource`
The processor’s reference to the source DOM node being transformed. Before the patch it held the caller’s node directly (mSource = &aSource), a non-owning pointer into the live document.
`txXPathNativeNode::createXPathNode`
Wraps a DOM nsINode* for the XPath/XSLT engine; the engine then walks this source tree.
XSLT + script re-entrancy
Transforming a live document can run author JavaScript (mutation observers, custom elements, etc.), which can mutate or detach the very nodes the transform is walking.

Root Cause Analysis

The XSLT transform held the source DOM by borrowed pointers while executing operations that can re-enter JavaScript, so script could free nodes still in use by the transform. In TransformToDocument, mSource = &aSource stored a bare pointer to the caller-supplied source node; the subsequent transform (TransformToDoc) walks that tree, and if script runs mid-transform and drops the source from the DOM, mSource dangles — a use-after-free. Similarly TransformToFragment built the XPath source directly from &aSource (createXPathNode(&aSource)), and the stylesheet compiler’s handleNode recursed over children using raw nsIContent* locals (for (nsIContent* child = element->GetFirstChild(); child; child = child->GetNextSibling())); a child freed by script during the recursive handleNode(child, ...) / characters(...) calls leaves the loop dereferencing freed memory. ensureStylesheet likewise held mEmbeddedStylesheetRoot via a raw nsINode*.

The patch makes the transform own stable copies and strong references. TransformToDocument now sets mSource = aSource.CloneNode(true, aRv) (a deep clone the processor owns), bailing on failure, so author script can no longer free the node the engine walks. TransformToFragment gains an aCloneSource parameter: the public XSLTProcessor entry point passes true (clone the source into a local nsCOMPtr<nsINode> source before createXPathNode(source)), while internal callers that supply a transient, non-script-reachable tree pass false to avoid the copy. nsXMLPrettyPrinter::PrettyPrint is updated to call the new 4-arg form with false. In the compiler, handleNode’s child-iteration locals become nsCOMPtr<nsIContent> child, and ensureStylesheet’s style becomes nsCOMPtr<nsINode>, so each node is kept alive across the re-entrant calls. This matches the advisory (UAF, CWE-416): the root mistake was lifetime, with the freed-memory/control-flow consequence inferred (the diff shows the lifetime fix, not an exploit).

Key insight
The XSLT processor walked the source DOM through non-owning raw pointers (mSource = &aSource, raw nsIContent* loop variables) while performing work that can re-enter author JavaScript, so script could delete nodes the transform was still using — a use-after-free cured by cloning the source and holding children via nsCOMPtr.

Attack Path

  1. Script A page uses XSLTProcessor (transformToDocument / transformToFragment) on a source node that is still attached to the live DOM, or triggers XSLT pretty-printing / stylesheet processing.
  2. Re-entrancy hook The source subtree carries script-observable triggers (mutation observers, custom-element callbacks, or similar) that run author JavaScript while the transform walks the tree (inferred mechanism; the diff establishes that script can run and that the pointers were non-owning).
  3. Free During the transform, that JavaScript detaches/destroys the source node(s) the engine is iterating, dropping their last owning reference.
  4. Dangling use The transform continues through mSource, createXPathNode(&aSource), or the raw nsIContent* child loop, dereferencing freed node memory.
  5. Primitive Use-after-free of DOM node objects in the renderer, exploitable for controlled memory corruption (strength inferred; the advisory rates it high).

Impact Assessment

The bug is in the content-process renderer (DOM XSLT) and is directly reachable by web content through the scriptable XSLTProcessor API and XSLT document/pretty-print paths — no elevated privilege required. The primitive is a use-after-free on freed DOM nodes; because an attacker can orchestrate the free via script and control subsequent allocations, UAFs of this kind are typically strong, but the actual exploitation strength is (inferred) — the diff proves the lifetime violation, not a working control-flow hijack. Reported by Project Zero, consistent with a serious renderer memory-safety flaw.

Changed Functions

FunctionChangeNotes
if
dom/xslt/xslt/txMozillaXSLTProcessor.cpp
modified

Files Changed

  • dom/xml/nsXMLPrettyPrinter.cpp
  • dom/xslt/xslt/txMozillaStylesheetCompiler.cpp
  • dom/xslt/xslt/txMozillaXSLTProcessor.cpp
  • dom/xslt/xslt/txMozillaXSLTProcessor.h

Audit Directions

  • Raw node pointers across script-running calls
    Audit XSLT/XPath engine code (txXPathNativeNode, txExecutionState, node-set construction) for bare nsINode*/nsIContent* held across calls that can execute author script.
  • Other `CloneNode`-vs-borrow entry points
    Review DOM APIs that accept a node and hand it to a long-running engine (serializers, range/selection ops, sanitizer) to confirm they own or strongly reference the input.
  • `GetFirstChild()`/`GetNextSibling()` loops
    Grep for sibling-iteration loops using raw locals that invoke callbacks (observers, listeners) in the body; these are the canonical re-entrancy UAF shape fixed here.
diff --git a/dom/xml/nsXMLPrettyPrinter.cpp b/dom/xml/nsXMLPrettyPrinter.cpp
index c587dd0e86b..b697acd6b51 100644
--- a/dom/xml/nsXMLPrettyPrinter.cpp
+++ b/dom/xml/nsXMLPrettyPrinter.cpp
@@ -80,7 +80,7 @@ nsresult nsXMLPrettyPrinter::PrettyPrint(Document* aDocument,
   }
 
   RefPtr<DocumentFragment> resultFragment =
-      transformer->TransformToFragment(*aDocument, *aDocument, err);
+      transformer->TransformToFragment(*aDocument, false, *aDocument, err);
   if (NS_WARN_IF(err.Failed())) {
     return err.StealNSResult();
   }
diff --git a/dom/xslt/xslt/txMozillaStylesheetCompiler.cpp b/dom/xslt/xslt/txMozillaStylesheetCompiler.cpp
index 1809668fedf..35933a17b38 100644
--- a/dom/xslt/xslt/txMozillaStylesheetCompiler.cpp
+++ b/dom/xslt/xslt/txMozillaStylesheetCompiler.cpp
@@ -486,7 +486,7 @@ static nsresult handleNode(nsINode* aNode, txStylesheetCompiler* aCompiler) {
     // explicitly destroy the attrs here since we no longer need it
     atts = nullptr;
 
-    for (nsIContent* child = element->GetFirstChild(); child;
+    for (nsCOMPtr<nsIContent> child = element->GetFirstChild(); child;
          child = child->GetNextSibling()) {
       rv = handleNode(child, aCompiler);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -500,7 +500,7 @@ static nsresult handleNode(nsINode* aNode, txStylesheetCompiler* aCompiler) {
     rv = aCompiler->characters(chars);
     NS_ENSURE_SUCCESS(rv, rv);
   } else if (aNode->IsDocument()) {
-    for (nsIContent* child = aNode->GetFirstChild(); child;
+    for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild(); child;
          child = child->GetNextSibling()) {
       rv = handleNode(child, aCompiler);
       NS_ENSURE_SUCCESS(rv, rv);
diff --git a/dom/xslt/xslt/txMozillaXSLTProcessor.cpp b/dom/xslt/xslt/txMozillaXSLTProcessor.cpp
index 906dcf2a3e8..50e755a4abf 100644
--- a/dom/xslt/xslt/txMozillaXSLTProcessor.cpp
+++ b/dom/xslt/xslt/txMozillaXSLTProcessor.cpp
@@ -550,7 +550,10 @@ already_AddRefed<Document> txMozillaXSLTProcessor::TransformToDocument(
     return nullptr;
   }
 
-  mSource = &aSource;
+  mSource = aSource.CloneNode(true, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
 
   nsCOMPtr<Document> doc;
   rv = TransformToDoc(getter_AddRefs(doc), true);
@@ -718,7 +721,7 @@ nsresult txMozillaXSLTProcessor::TransformToDoc(Document** aResult,
 }
 
 already_AddRefed<DocumentFragment> txMozillaXSLTProcessor::TransformToFragment(
-    nsINode& aSource, Document& aOutput, ErrorResult& aRv) {
+    nsINode& aSource, bool aCloneSource, Document& aOutput, ErrorResult& aRv) {
   if (NS_WARN_IF(NS_FAILED(mCompileResult))) {
     aRv.Throw(mCompileResult);
     return nullptr;
@@ -738,8 +741,17 @@ already_AddRefed<DocumentFragment> txMozillaXSLTProcessor::TransformToFragment(
     return nullptr;
   }
 
-  UniquePtr<txXPathNode> sourceNode(
-      txXPathNativeNode::createXPathNode(&aSource));
+  nsCOMPtr<nsINode> source;
+  if (aCloneSource) {
+    source = aSource.CloneNode(true, aRv);
+    if (aRv.Failed()) {
+      return nullptr;
+    }
+  } else {
+    source = &aSource;
+  }
+
+  UniquePtr<txXPathNode> sourceNode(txXPathNativeNode::createXPathNode(source));
   if (!sourceNode) {
     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return nullptr;
@@ -1046,7 +1058,7 @@ nsresult txMozillaXSLTProcessor::ensureStylesheet() {
 
   NS_ENSURE_TRUE(mStylesheetDocument, NS_ERROR_NOT_INITIALIZED);
 
-  nsINode* style = mEmbeddedStylesheetRoot;
+  nsCOMPtr<nsINode> style = mEmbeddedStylesheetRoot;
   if (!style) {
     style = mStylesheetDocument;
   }
diff --git a/dom/xslt/xslt/txMozillaXSLTProcessor.h b/dom/xslt/xslt/txMozillaXSLTProcessor.h
index b000f804c61..c8ef94aae79 100644
--- a/dom/xslt/xslt/txMozillaXSLTProcessor.h
+++ b/dom/xslt/xslt/txMozillaXSLTProcessor.h
@@ -94,7 +94,12 @@ class txMozillaXSLTProcessor final : public nsIDocumentTransformer,
 
   void ImportStylesheet(nsINode& stylesheet, mozilla::ErrorResult& aRv);
   already_AddRefed<mozilla::dom::DocumentFragment> TransformToFragment(
-      nsINode& source, mozilla::dom::Document& docVal,
+      nsINode& aSource, mozilla::dom::Document& aDocument,
+      mozilla::ErrorResult& aRv) {
+    return TransformToFragment(aSource, true, aDocument, aRv);
+  }
+  already_AddRefed<mozilla::dom::DocumentFragment> TransformToFragment(
+      nsINode& aSource, bool aCloneSource, mozilla::dom::Document& aOutput,
       mozilla::ErrorResult& aRv);
   already_AddRefed<mozilla::dom::Document> TransformToDocument(
       nsINode& source, mozilla::ErrorResult& aRv);
Loading diff…