High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DOM
DescriptionUse after free in DOM
ComponentDOM
Sub-componentXSLT
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Additionally cross-checked: a second, different model independently derived the same bug class, vulnerable function and fix from the commit.
Tracker496271580
Fix commit6acb62a1f250 (chromium/src) +66/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-05-27
Patch gap12 days — fix public in source before the advisory

Background

`ProcessingInstruction::ProcessStylesheet`
the method that handles an <?xml-stylesheet?> PI, branching on whether the href is a local anchor or an external URL (processing_instruction.cc:386).
`XSLStyleSheet::LoadChildSheets`
walks the parsed stylesheet’s libxml2 xmlNode tree and synchronously fetches each xsl:import/xsl:include child (xsl_style_sheet_libxslt.cc:163).
`stylesheet_doc_taken_`
a flag meaning libxslt has taken ownership of (and may have freed) the sheet’s xmlDoc; the destructor skips xmlFreeDoc when it is set (xsl_style_sheet_libxslt.cc:98).
`ClearResource`
detaches the ProcessingInstruction as a client of its pending stylesheet fetch so a late response is not processed.

Root Cause Analysis

Verify before building a PoC

The fix in this commit is confirmed, but the trigger and sink live in code this commit does not touch — find and confirm them against the source before building a PoC.

In the pre-fix ProcessStylesheet, the local-sheet branch (processing_instruction.cc:389) sets loading_ = false at processing_instruction.cc:398 and returns at processing_instruction.cc:401 without ever calling ClearResource(); only the external branch calls ClearResource() at processing_instruction.cc:403. If a PI switches from an external href to a local one while a fetch is in flight, the object stays registered as a client of that stale fetch. When the late response arrives, NotifyFinished builds a new XSLStyleSheet and calls ParseString (processing_instruction.cc:454-457), which runs LoadChildSheets. Inside the import and include loops, LoadChildSheet is called synchronously (xsl_style_sheet_libxslt.cc:199 and :213); that call can recurse into an XSL transformation that takes and frees the parent sheet’s stylesheet_doc_, yet the pre-fix loop keeps iterating with curr = curr->next (xsl_style_sheet_libxslt.cc:204,:216) over the now-freed libxml2 tree. The invariant — that the xmlDoc being iterated stays alive for the duration of the loop — is violated.

The fix restores it two ways: ClearResource() is added before the local-sheet return (diff at processing_instruction.cc @@ -397,6) to stop stale responses being processed, and a if (stylesheet_doc_taken_) { return; } guard is added after each LoadChildSheet()/xmlFree() in both loops (diff at xsl_style_sheet_libxslt.cc @@ -198,6 and @@ -212,6) to bail out the instant the doc has been taken.

Key insight
The loop iterating the stylesheet’s libxml2 xmlDoc never re-checked whether a synchronous LoadChildSheet() had caused that doc to be freed, and a PI switching to a local href left a stale fetch armed; the patch both clears the stale resource and makes the loop bail once stylesheet_doc_taken_ is set.

Attack Path

  1. Start an external XSL fetch A PI points at an external, deliberately slow .xsl, so ProcessStylesheet takes the external branch, sets loading_ = true and begins the fetch (processing_instruction.cc:410).
  2. Switch to a local href mid-flight Script rewrites the PI’s data to a local # anchor; the local branch sets loading_ = false and returns without ClearResource(), leaving the stale fetch armed (processing_instruction.cc:389-401).
  3. Stale response parses a stylesheet When the delayed response arrives, NotifyFinished creates an XSLStyleSheet and calls ParseString, which invokes LoadChildSheets (processing_instruction.cc:454-457).
  4. Synchronous child load frees the doc Processing an xsl:import child via LoadChildSheet (xsl_style_sheet_libxslt.cc:199) recurses into a transform that frees the parent’s stylesheet_doc_ while the loop is still active. (inferred)
  5. Use-after-free on continued iteration Execution returns to the loop, which advances curr = curr->next over the freed libxml2 node tree (xsl_style_sheet_libxslt.cc:204). Memory corruption beyond this point is (inferred).

Impact Assessment

An attacker-controlled page can cause a use-after-free of libxml2 xmlNode/xmlDoc memory in the renderer process. Because these objects are C malloc allocations outside Blink’s garbage collector, the freed memory is a corruption primitive; the advisory rates this high severity. The advisory records no in-the-wild exploitation; escalation to code execution is (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
modified
if
third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml
modified

Files Changed

  • third_party/blink/renderer/core/dom/processing_instruction.cc
  • third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
  • third_party/blink/web_tests/external/wpt/lint.ignore
  • third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl
  • third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl
  • third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml

Audit Directions

  • Synchronous reentrancy during iteration
    When a loop iterates a C-allocated tree (libxml2) and calls out to code that can synchronously free that tree, re-check a liveness flag after every such call before dereferencing the next node.
  • State transitions that disarm guards
    When an object can switch modes (external to local fetch) and a mode flag like loading_ is cleared, verify that every associated resource/client is also released, not just the flag.
  • Non-GC memory in a GC world
    Treat objects owned outside Oilpan (libxml2/libxslt malloc allocations) as unprotected by MiraclePtr and audit every path where their lifetime can end mid-operation.
From 6acb62a1f2500f44c446f301d71575c5b9916987 Mon Sep 17 00:00:00 2001
From: Keishi Hattori <[email protected]>
Date: Thu, 14 May 2026 22:10:13 -0700
Subject: [PATCH] [XSLT] Mitigate potential UAF in XSLStyleSheet::LoadChildSheets

This CL mitigates UAF in XSLStyleSheet by adding liveness checks during
child sheet loading. Synchronous recursive destruction could occur when
a child sheet triggers an XSL transformation that frees the parent
stylesheet's xmlDoc while it was still being iterated.

Key changes:
- Added checks for stylesheet_doc_taken_ in LoadChildSheets loop.
- Added missing ClearResource() call in ProcessingInstruction when
  switching to a local stylesheet to prevent processing stale network
  responses.

Bug: 496271580
Change-Id: Ib579489073a9e71dc9433307e3238db2939a14ba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7702938
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Keishi Hattori <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1631070}
---

diff --git a/third_party/blink/renderer/core/dom/processing_instruction.cc b/third_party/blink/renderer/core/dom/processing_instruction.cc
index a0175d40..738b036d 100644
--- a/third_party/blink/renderer/core/dom/processing_instruction.cc
+++ b/third_party/blink/renderer/core/dom/processing_instruction.cc
@@ -397,6 +397,11 @@
                                                    final_url, true);
       loading_ = false;
     }
+
+    // crbug.com/496271580: Clear the resource to prevent late-arriving
+    // network responses from being processed if the stylesheet has
+    // switched to a local source.
+    ClearResource();
     return;
   }
 
diff --git a/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc b/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
index 4bc346b..64f72a6 100644
--- a/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
+++ b/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
@@ -198,6 +198,12 @@
             xsltGetNsProp(curr, (const xmlChar*)"href", XSLT_NAMESPACE);
         LoadChildSheet(String::FromUtf8((const char*)uri_ref));
         xmlFree(uri_ref);
+
+        // crbug.com/496271580: LoadChildSheet() can trigger synchronous
+        // destruction of the stylesheet's xmlDoc. Bail out to avoid UAF.
+        if (stylesheet_doc_taken_) {
+          return;
+        }
       } else {
         break;
       }
@@ -212,6 +218,12 @@
             xsltGetNsProp(curr, (const xmlChar*)"href", XSLT_NAMESPACE);
         LoadChildSheet(String::FromUtf8((const char*)uri_ref));
         xmlFree(uri_ref);
+
+        // crbug.com/496271580: LoadChildSheet() can trigger synchronous
+        // destruction of the stylesheet's xmlDoc. Bail out to avoid UAF.
+        if (stylesheet_doc_taken_) {
+          return;
+        }
       }
       curr = curr->next;
     }
diff --git a/third_party/blink/web_tests/external/wpt/lint.ignore b/third_party/blink/web_tests/external/wpt/lint.ignore
index 7b21a36c..88789eb 100644
--- a/third_party/blink/web_tests/external/wpt/lint.ignore
+++ b/third_party/blink/web_tests/external/wpt/lint.ignore
@@ -444,6 +444,7 @@
 SET TIMEOUT: speculation-rules/prerender/resources/media-play.html
 SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/timers.html
 SET TIMEOUT: dom/abort/crashtests/timeout-close.html
+SET TIMEOUT: xml/xslt/xslt-mutation-crash.xhtml
 SET TIMEOUT: storage-access-api/storage-access-beyond-cookies.locks.sub.https.window.js
 SET TIMEOUT: pointerevents/crashtests/longpress-crash.html
 
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl
new file mode 100644
index 0000000..e3d28506
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+</xsl:stylesheet>
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl
new file mode 100644
index 0000000..c224799
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:import href="xslt-mutation-crash-import.xsl"/>
+  <xsl:template match="/">
+    <html><body>hello world</body></html>
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml
new file mode 100644
index 0000000..cd20c54
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml" class="test-wait">
+<head>
+  <title>XSLT LoadChildSheets crash test</title>
+  <link rel="help" href="https://crbug.com/496271580"/>
+</head>
+<body>
+<p>Test passes if it does not crash.</p>
+<script><![CDATA[
+function go() {
+  // Unique URL to avoid memory cache returning synchronously.
+  var slow = 'xslt-mutation-crash-slow.xsl?pipe=trickle(d1)&t=' + Date.now() + Math.random();
+
+  // 1. Create an xml-stylesheet PI pointing at an external (delayed) XSL.
+  var pi = document.createProcessingInstruction(
+      'xml-stylesheet', 'type="text/xsl" href="' + slow + '"');
+
+  // 2. Insert as a direct child of the Document to initiate the external fetch.
+  document.insertBefore(pi, document.documentElement);
+
+  // 3. Mutate the PI to a local href while the external fetch is in flight.
+  //    This verifies that the browser correctly detaches the pending network
+  //    resource and does not crash when the delayed response eventually arrives.
+  pi.data = 'type="text/xsl" href="#x"';
+
+  // 4. Wait 1.5 seconds for the trickle(d1) fetch to complete and verify no crash occurs.
+  setTimeout(() => {
+    document.documentElement.classList.remove('test-wait');
+  }, 1500);
+}
+if (document.readyState === 'complete') {
+  go();
+} else {
+  window.addEventListener('load', go);
+}
+]]></script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/lint.ignore b/third_party/blink/web_tests/external/wpt/lint.ignore
index 7b21a36c..88789eb 100644
--- a/third_party/blink/web_tests/external/wpt/lint.ignore
+++ b/third_party/blink/web_tests/external/wpt/lint.ignore
@@ -444,6 +444,7 @@
 SET TIMEOUT: speculation-rules/prerender/resources/media-play.html
 SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/timers.html
 SET TIMEOUT: dom/abort/crashtests/timeout-close.html
+SET TIMEOUT: xml/xslt/xslt-mutation-crash.xhtml
 SET TIMEOUT: storage-access-api/storage-access-beyond-cookies.locks.sub.https.window.js
 SET TIMEOUT: pointerevents/crashtests/longpress-crash.html
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl
new file mode 100644
index 0000000..e3d28506
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-import.xsl
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+</xsl:stylesheet>
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl
new file mode 100644
index 0000000..c224799
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash-slow.xsl
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:import href="xslt-mutation-crash-import.xsl"/>
+  <xsl:template match="/">
+    <html><body>hello world</body></html>
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml
new file mode 100644
index 0000000..cd20c54
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/xslt-mutation-crash.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml" class="test-wait">
+<head>
+  <title>XSLT LoadChildSheets crash test</title>
+  <link rel="help" href="https://crbug.com/496271580"/>
+</head>
+<body>
+<p>Test passes if it does not crash.</p>
+<script><![CDATA[
+function go() {
+  // Unique URL to avoid memory cache returning synchronously.
+  var slow = 'xslt-mutation-crash-slow.xsl?pipe=trickle(d1)&t=' + Date.now() + Math.random();
+
+  // 1. Create an xml-stylesheet PI pointing at an external (delayed) XSL.
+  var pi = document.createProcessingInstruction(
+      'xml-stylesheet', 'type="text/xsl" href="' + slow + '"');
+
+  // 2. Insert as a direct child of the Document to initiate the external fetch.
+  document.insertBefore(pi, document.documentElement);
+
+  // 3. Mutate the PI to a local href while the external fetch is in flight.
+  //    This verifies that the browser correctly detaches the pending network
+  //    resource and does not crash when the delayed response eventually arrives.
+  pi.data = 'type="text/xsl" href="#x"';
+
+  // 4. Wait 1.5 seconds for the trickle(d1) fetch to complete and verify no crash occurs.
+  setTimeout(() => {
+    document.documentElement.classList.remove('test-wait');
+  }, 1500);
+}
+if (document.readyState === 'complete') {
+  go();
+} else {
+  window.addEventListener('load', go);
+}
+]]></script>
+</body>
+</html>
Loading diff…

Original Bug Report

reported by [email protected]

UAF in XSLStyleSheet::LoadChildSheets via Stale ResourceClient

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A logic error in ProcessingInstruction::ProcessStylesheet allows a stale resource fetch to trigger an XSL transformation prematurely. This transformation frees the stylesheet document while it is still being iterated in XSLStyleSheet::LoadChildSheets, leading to a heap-use-after-free of libxml2 xmlNodes.

Affected files:

  • third_party/blink/renderer/core/dom/processing_instruction.cc
  • third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
  • third_party/blink/renderer/core/xml/xslt_processor_libxslt.cc
  • third_party/blink/renderer/core/xml/document_xslt.cc

Estimated timestamp from git blame: 2026-03-11

Summary

A potential heap-use-after-free (UAF) vulnerability exists in XSLStyleSheet::LoadChildSheets. The issue stems from a logic flaw in ProcessingInstruction::ProcessStylesheet where mutating a stylesheet’s href from an external URL to a local anchor (e.g., #id) disarms the loading_ state guard but fails to clear the pending ResourceClient.

When the stale external fetch subsequently completes, it prematurely triggers an XSL transformation. This transformation frees the underlying libxml2 xmlDoc tree while LoadChildSheets is still iterating over its nodes. Because libxml2 objects are allocated via C malloc, they are not protected by MiraclePtr (BRP), making this a highly exploitable UAF in the renderer process.

Root Cause Analysis

In third_party/blink/renderer/core/dom/processing_instruction.cc, the ProcessStylesheet method handles two main branches: external and local.

  1. When an external href is processed, ClearResource() is called to detach previous clients, and loading_ is set to true.
  2. When a local href (starting with #) is processed, the code creates a local XSLStyleSheet, sets loading_ = false, and returns early without calling ClearResource().

If a script mutates the ProcessingInstruction data from an external URL to a local one while a fetch is in-flight, the loading_ guard is disarmed, but the stale ResourceClient remains registered. When the external fetch completes, NotifyFinished() is called. Because loading_ is now false, guards in SheetLoaded and ApplyXSLTransform are bypassed.

Potential Attack Scenario

Note: Our setup cannot currently verify this with a working exploit, but the code path appears highly reliable.

  1. Preparation: An attacker creates an HTML page embedding an XML document with a <?xml-stylesheet type="text/xsl" href="external.xsl"?> ProcessingInstruction (PI). The external.xsl server is configured to delay its response.
  2. External Fetch Initiated: ProcessingInstruction::ProcessStylesheet sets loading_ = true and initiates the asynchronous fetch for external.xsl.
  3. State Mutation: Before the fetch completes, the attacker uses synchronous JavaScript to mutate the PI’s data attribute: pi.data = 'type="text/xsl" href="#local"';.
  4. Guard Disarmed: This triggers a second call to ProcessStylesheet("#local"). Because it’s a local href, the function sets loading_ = false and returns early, bypassing the crucial ClearResource() call. The PI remains registered for the delayed external.xsl fetch.
  5. Fetch Completion: The external.xsl fetch finishes, triggering ProcessingInstruction::NotifyFinished. This creates a new XSLStyleSheet and calls ParseString(), which parses the XML via libxml2 into an xmlDocPtr.
  6. Child Iteration: ParseString calls LoadChildSheets(), which begins a while loop iterating over the xmlNodePtr children of the parsed document.
  7. Premature Transformation: The loop encounters an <xsl:import> element and synchronously fetches it. This bubbles up a CheckLoaded() call. Because loading_ was maliciously set to false in Step 4, ProcessingInstruction::IsLoading() returns false.
  8. UAF Trigger: Believing all stylesheets are fully loaded, DocumentXSLT::ApplyXSLTransform is called. It compiles the stylesheet, transferring ownership of the xmlDocPtr to libxslt, performs the transformation, and finally calls xsltFreeStylesheet, which entirely frees the xmlDocPtr tree via malloc.
  9. Synchronous Execution: The transformation result is committed via a frame navigation. The attacker’s XSLT output contains a <script> tag, which executes synchronously, allowing the attacker to groom the heap and perfectly reclaim the newly freed xmlNodePtr chunks.
  10. The Crash/Exploit: The call stack unwinds back to the while loop in LoadChildSheets(). The code executes curr = curr->next; on the attacker-controlled, freed memory, leading to an arbitrary read/write primitive and RCE.

Suggested Fix

Ensure ClearResource() is always called in ProcessingInstruction::ProcessStylesheet before the local-href early return path. This guarantees that no stale fetches can trigger callbacks while the object is transitioning to an inconsistent state.

void ProcessingInstruction::ProcessStylesheet(const String& href,
                                              const String& charset) {
  CHECK(IsXMLStylesheet());
  if (IsLocalSheet(href)) {
    ClearResource(); // <--- Add this line to clear any pending fetches
    local_href_ = href.substr(1);
    // ... rest of the local sheet handling
    return;
  }

  ClearResource();
  // ... external sheet handling
}

Evaluated with Chrome root at commit: a3f5fcb392f2902650ca2b71820e7e418787e18b


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker