Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in XML
DescriptionUse after free in XML
ComponentXML
Bug ClassUAF
Tracker503627446
Fix commitcd30fc331762 (chromium/src) +60/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • third_party/blink/web_tests/TestExpectations
  • third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html
  • third_party/libxslt/chromium/0005-Verify-dictionary-ownership-before-aliasing-attribut.patch
  • third_party/libxslt/chromium/roll.py
  • third_party/libxslt/src/libxslt/attributes.c
From cd30fc3317627618d206710777b7e9909d171af7 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Wed, 22 Apr 2026 14:51:09 -0700
Subject: [PATCH] Verify dictionary ownership before aliasing attribute text content

A potential double-free vulnerability exists in libxslt when handling
whitespace-only text nodes within xsl:attribute elements that have
xml:space="preserve" set. The text content bypasses dictionary interning
but is later fast-copied via pointer aliasing, causing the same heap
buffer to be freed twice during cleanup.

This patch adds a check to ensure that the content is actually owned by
the dictionary before performing the fast-path pointer aliasing.

Fixed: 503627446
Change-Id: I5e8835f1f0b81fb9314d48daac05d6622d64ec60
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7778588
Commit-Queue: Andrew Paseltiner <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: James Scott <[email protected]>
Reviewed-by: Nico Weber <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619107}
---

diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 64069a6..6771a92b 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1665,6 +1665,7 @@
 # `xslt-disabled` virtual suite re-enables the XSLTSpecialTrial flag and
 # disables the main XSLT flag, which should keep XSLT disabled.
 crbug.com/421650040 external/wpt/xml/xslt/* [ Failure ]
+crbug.com/503627446 external/wpt/xml/xslt/xslt-attribute-double-free-crash.html [ Pass ]
 crbug.com/421650040 virtual/xslt-enabled/external/wpt/xml/xslt/* [ Pass ]
 crbug.com/421650040 http/tests/xsl/xslt/* [ Failure ]
 crbug.com/421650040 virtual/xslt-enabled/http/tests/xsl/xslt/* [ Pass ]
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html
new file mode 100644
index 0000000..e96b9c00
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>XSLT attribute double-free crash test</title>
+<link rel="help" href="https://crbug.com/503627446">
+<script>
+function runTest() {
+    // The crash only occurs with > 60 characters, as otherwise an optimization is bypassed.
+    const xsl =
+      "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
+      "<xsl:template match=\"/\"><r><xsl:for-each select=\"//*\">" +
+      "<e><xsl:attribute name=\"a\" xml:space=\"preserve\">" + " ".repeat(64) + "</xsl:attribute></e>" +
+      "</xsl:for-each></r></xsl:template></xsl:stylesheet>";
+
+    const p = new DOMParser();
+    const proc = new XSLTProcessor();
+
+    proc.importStylesheet(p.parseFromString(xsl, "text/xml"));
+    proc.transformToDocument(p.parseFromString("<r><a></a><b></b></r>", "text/xml"));
+}
+</script>
+<body onload="runTest()">
+</body>
diff --git a/third_party/libxslt/chromium/0005-Verify-dictionary-ownership-before-aliasing-attribut.patch b/third_party/libxslt/chromium/0005-Verify-dictionary-ownership-before-aliasing-attribut.patch
new file mode 100644
index 0000000..f3d92c2
--- /dev/null
+++ b/third_party/libxslt/chromium/0005-Verify-dictionary-ownership-before-aliasing-attribut.patch
@@ -0,0 +1,35 @@
+From 86951e62ddbf3667683d10387f3667683d10387f Mon Sep 17 00:00:00 2001
+From: Andrew Paseltiner <[email protected]>
+Date: Mon, 20 Apr 2026 08:45:04 -0400
+Subject: [PATCH] Verify dictionary ownership before aliasing attribute text content
+
+A potential double-free vulnerability exists in libxslt when handling
+whitespace-only text nodes within xsl:attribute elements that have
+xml:space="preserve" set. The text content bypasses dictionary interning
+but is later fast-copied via pointer aliasing, causing the same heap
+buffer to be freed twice during cleanup.
+
+This patch adds a check to ensure that the content is actually owned by
+the dictionary before performing the fast-path pointer aliasing.
+
+Bug: 503627446
+---
+ third_party/libxslt/src/libxslt/attributes.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/third_party/libxslt/src/libxslt/attributes.c b/third_party/libxslt/src/libxslt/attributes.c
+index 86f3768b..3667683d 100644
+--- a/libxslt/attributes.c
++++ b/libxslt/attributes.c
+@@ -1038,7 +1038,8 @@ xsltAttribute(xsltTransformContextPtr ctxt, xmlNodePtr node,
+ 	*/
+ 	if (ctxt->internalized &&
+ 	    (ctxt->insert->doc != NULL) &&
+-	    (ctxt->insert->doc->dict == ctxt->dict))
++	    (ctxt->insert->doc->dict == ctxt->dict) &&
++	    xmlDictOwns(ctxt->dict, inst->children->content))
+ 	{
+ 	    copyTxt = xmlNewText(NULL);
+ 	    if (copyTxt == NULL) /* TODO: report error */
+-- 
+2.50.0.rc0.642.g800a2b2222-goog
diff --git a/third_party/libxslt/chromium/roll.py b/third_party/libxslt/chromium/roll.py
index 4244b56..410762e 100755
--- a/third_party/libxslt/chromium/roll.py
+++ b/third_party/libxslt/chromium/roll.py
@@ -73,6 +73,7 @@
 PATCHES = [
     'xslt-locale.patch',
     '0004-Use-a-dedicated-node-type-to-maintain-the-list-of-ca.patch',
+    '0005-Verify-dictionary-ownership-before-aliasing-attribut.patch',
 ]
 
 
diff --git a/third_party/libxslt/src/libxslt/attributes.c b/third_party/libxslt/src/libxslt/attributes.c
index 4cc49d0d..fb15a5b0 100644
--- a/third_party/libxslt/src/libxslt/attributes.c
+++ b/third_party/libxslt/src/libxslt/attributes.c
@@ -1036,7 +1036,8 @@
 	*/
 	if (ctxt->internalized &&
 	    (ctxt->insert->doc != NULL) &&
-	    (ctxt->insert->doc->dict == ctxt->dict))
+	    (ctxt->insert->doc->dict == ctxt->dict) &&
+	    xmlDictOwns(ctxt->dict, inst->children->content))
 	{
 	    copyTxt = xmlNewText(NULL);
 	    if (copyTxt == NULL) /* TODO: report error */
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 64069a6..6771a92b 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1665,6 +1665,7 @@
 # `xslt-disabled` virtual suite re-enables the XSLTSpecialTrial flag and
 # disables the main XSLT flag, which should keep XSLT disabled.
 crbug.com/421650040 external/wpt/xml/xslt/* [ Failure ]
+crbug.com/503627446 external/wpt/xml/xslt/xslt-attribute-double-free-crash.html [ Pass ]
 crbug.com/421650040 virtual/xslt-enabled/external/wpt/xml/xslt/* [ Pass ]
 crbug.com/421650040 http/tests/xsl/xslt/* [ Failure ]
 crbug.com/421650040 virtual/xslt-enabled/http/tests/xsl/xslt/* [ Pass ]
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html
new file mode 100644
index 0000000..e96b9c00
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-attribute-double-free-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>XSLT attribute double-free crash test</title>
+<link rel="help" href="https://crbug.com/503627446">
+<script>
+function runTest() {
+    // The crash only occurs with > 60 characters, as otherwise an optimization is bypassed.
+    const xsl =
+      "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
+      "<xsl:template match=\"/\"><r><xsl:for-each select=\"//*\">" +
+      "<e><xsl:attribute name=\"a\" xml:space=\"preserve\">" + " ".repeat(64) + "</xsl:attribute></e>" +
+      "</xsl:for-each></r></xsl:template></xsl:stylesheet>";
+
+    const p = new DOMParser();
+    const proc = new XSLTProcessor();
+
+    proc.importStylesheet(p.parseFromString(xsl, "text/xml"));
+    proc.transformToDocument(p.parseFromString("<r><a></a><b></b></r>", "text/xml"));
+}
+</script>
+<body onload="runTest()">
+</body>
Loading diff…

Original Bug Report

reported by [email protected]

Potential double free in libxslt via xsl:attribute and xml:space="preserve"

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential double-free vulnerability exists in libxslt when handling whitespace-only text nodes within xsl:attribute elements that have xml:space="preserve" set. The text content bypasses dictionary interning but is later fast-copied via pointer aliasing, causing the same heap buffer to be freed twice during cleanup. On standard Android, where MiraclePtr is disabled for the renderer, this corrupts the PartitionAlloc thread cache, potentially leading to overlapping allocations and renderer Remote Code Execution.

Affected files:

  • third_party/libxslt/src/libxslt/attributes.c
  • third_party/libxslt/src/libxslt/xslt.c
  • third_party/libxml/src/tree.c
  • third_party/blink/renderer/core/xml/xslt_processor_libxslt.cc

Estimated timestamp from git blame: 2015-09-16

Summary

A potential double-free vulnerability has been identified in the XSLT processing logic used by Chromium. The issue occurs due to an assumption mismatch in libxslt regarding dictionary interning. When a whitespace-only text node inside an xsl:attribute element preserves space, its content is allocated on the heap without being added to the parser dictionary. However, during transformation, the attribute logic assumes the content is dictionary-backed and aliases the raw pointer directly into the result tree. When Blink cleans up the result document and the stylesheet, the exact same heap pointer is passed to xmlFree twice.

While MiraclePtr mitigates this issue into a safe crash on Desktop platforms, standard Android builds disable MiraclePtr for the renderer process, allowing this to be leveraged for overlapping memory allocations and Remote Code Execution.

Root Cause Analysis

The vulnerability traces through three main phases:

1. Allocation and Parsing (third_party/libxml/src/SAX2.c) When libxml2 parses an XSLT document, the xmlSAX2TextNode callback attempts to intern text into the document’s dictionary. For whitespace-only strings (“blank” nodes), it checks if len < 60. If an attacker provides 60 or more whitespace characters, this optimization is bypassed, and the string is allocated on the heap via xmlStrndup.

2. Stylesheet Preprocessing (third_party/libxslt/src/libxslt/xslt.c) During xsltPreprocessStylesheet, the engine processes the stylesheet tree. At line 3613, it encounters the whitespace text node:

} else if (cur->type == XML_TEXT_NODE) {
    if (IS_BLANK_NODE(cur)) {
        if (xmlNodeGetSpacePreserve(cur->parent) != 1) {
            deleteNode = cur;
        }
    } else if ((cur->content != NULL) && (internalize) && (!xmlDictOwns(style->dict, cur->content))) {
        // ... intern the string ...
    }

Because the node contains only whitespace (IS_BLANK_NODE is true) and the attacker explicitly set xml:space="preserve" on the parent xsl:attribute, the node is kept. Crucially, because it entered the if (IS_BLANK_NODE) branch, it bypasses the else if branch responsible for interning heap-allocated strings into the dictionary. The node retains its raw heap pointer.

3. Attribute Generation (third_party/libxslt/src/libxslt/attributes.c) During the transformation, xsltAttribute evaluates the attribute’s children to attach them to the result tree. At line 1037:

if (ctxt->internalized && (ctxt->insert->doc != NULL) && (ctxt->insert->doc->dict == ctxt->dict)) {
    copyTxt = xmlNewText(NULL);
    copyTxt->content = inst->children->content; // Direct pointer aliasing

Because ctxt->internalized is true (inherited from the stylesheet initialization), libxslt assumes the string is safely interned in the dictionary and performs a direct pointer alias instead of a deep copy. The result tree now contains a node pointing to the exact same heap allocation as the stylesheet tree.

Double Free Trigger

When the transformation completes, XSLTProcessor::TransformToString in Blink performs cleanup:

  1. It calls xmlFreeDoc(result_doc). The DICT_FREE macro checks if the pointer is owned by the dictionary (it is not) and calls xmlFree, returning the chunk to PartitionAlloc’s thread cache.
  2. Immediately after, it calls xsltFreeStylesheet(sheet). This traverses the stylesheet tree, finds the original text node, and calls xmlFree on the exact same pointer again, triggering a synchronous double free.

Exploitability and Platform Impact

  • Desktop/ChromeOS: MiraclePtr (BackupRefPtr) is enabled for the renderer process. The second free attempts to release the chunk, but ReleaseFromAllocator detects that kMemoryHeldByAllocatorBit is already cleared. This correctly triggers a deterministic PA_IMMEDIATE_CRASH(), resulting only in a safe Denial of Service.
  • Standard Android: MiraclePtr is explicitly disabled for the renderer process (BackupRefPtrEnabledProcesses::kNonRenderer in partition_alloc_features.cc). Consequently, the double-free check is bypassed. PartitionAlloc’s ThreadCache::PutInBucket pushes the same pointer onto the freelist twice. This encodes the next pointer to point to itself (entry->next = entry). Because the shadow entry remains perfectly consistent with this corrupted next pointer, standard release builds will not crash during reallocation.

An attacker can trigger this vulnerability and subsequently request allocations of the same size class via Javascript. The corrupted freelist cycle will cause GetFromCache to repeatedly return the same memory address, granting the attacker overlapping objects. This is a classic and highly reliable primitive for achieving Remote Code Execution within the sandboxed Android renderer process.

Potential Reproduction Steps

(Note: These are theoretical steps based on code analysis; our tooling agent cannot execute code to verify the PoC.)

<script>
// 1. Create a stylesheet with >60 spaces inside an xsl:attribute preserving space.
var xsl = 
  "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
  "<xsl:template match=\"/\"><r><xsl:for-each select=\"//*\">" +
  "<e><xsl:attribute name=\"a\" xml:space=\"preserve\">" + " ".repeat(64) + "</xsl:attribute></e>" +
  "</xsl:for-each></r></xsl:template></xsl:stylesheet>";

var p = new DOMParser();
var proc = new XSLTProcessor();
// 2. Import stylesheet (parses and skips interning)
proc.importStylesheet(p.parseFromString(xsl, "text/xml"));
// 3. Transform (aliases pointer, then double-frees during Blink cleanup)
proc.transformToDocument(p.parseFromString("<r><a/><b/></r>", "text/xml"));
</script>

Proposed Fix

The mismatch between preprocessing and attribute generation must be resolved in libxslt. One potential fix in third_party/libxslt/src/libxslt/attributes.c is to verify dictionary ownership before taking the fast path:

if (ctxt->internalized &&
    (ctxt->insert->doc != NULL) &&
    (ctxt->insert->doc->dict == ctxt->dict) &&
    xmlDictOwns(ctxt->dict, inst->children->content)) // ADD THIS CHECK
{
    copyTxt = xmlNewText(NULL);
    copyTxt->content = inst->children->content;
    // ...
}

Alternatively, xsltPreprocessStylesheet could be updated to ensure that nodes bypassing the deleteNode logic due to xml:space="preserve" are still correctly interned into the dictionary if their content is currently heap-allocated.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker