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 Script
DescriptionUse after free in Script
ComponentScript
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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.
Tracker501892500
Fix commit506527a4327d (chromium/src) +12/-1
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-04-12 (per vendor advisory)
Fix landed2026-07-09 (public git)
Disclosed2026-08-25
Patch gap47 days — fix public in source before the advisory

Background

`xmlParseChunk` (libxml2 push parser)
The incremental XML parsing entry point that consumes a chunk of bytes into a parser context (ctxt) and fires SAX callbacks; it is not designed to be re-entered while a callback is executing.
Parser context arrays (`pushTab`/`nsTab`)
Internal ctxt buffers holding raw pointers that libxml2 may xmlRealloc() (grow) during parsing, invalidating any pointers captured before the reallocation.
`XMLDocumentParser::DoWrite`
Blink’s wrapper that feeds source bytes to xmlParseChunk; SAX callbacks it triggers can run script/JS-observable logic that may re-enter Blink’s parser.
`XMLDocumentParser::Append` / `pending_src_`
The entry point that receives incoming XML source; pending_src_ is a buffer where source is queued instead of being parsed immediately when the parser cannot safely run.

Root Cause Analysis

XMLDocumentParser::DoWrite invokes libxml2’s non-re-entrant xmlParseChunk, which fires SAX callbacks while holding raw pointers into ctxt arrays; if such a callback re-enters Blink and drives another xmlParseChunk (a nested chunk parse), libxml2 can xmlRealloc() those arrays out from under the outer, still-executing xmlParseEndTag2, leaving it dereferencing freed/moved buffers — a use-after-free / heap corruption. Before the patch, Append only diverted incoming source to pending_src_ when parser_paused_ was set, so a re-entrant write that arrived while a chunk parse was in progress (but the parser was not “paused”) would proceed straight into a nested xmlParseChunk.

The fix adds an in_parse_chunk_ boolean, sets it via base::AutoReset for the duration of DoWrite’s xmlParseChunk call, and asserts CHECK(!in_parse_chunk_) on entry; it also augments Append to route source to pending_src_ when in_parse_chunk_ is true. Together these ensure re-entrant source is queued rather than parsed, so the outer parse’s ctxt pointers are never reallocated mid-flight, and if re-entrancy somehow still reaches DoWrite the CHECK fails safely instead of corrupting the heap.

Key insight
The core mistake was allowing re-entrant XML input to drive a nested xmlParseChunk while an outer chunk parse still held raw pointers into reallocatable ctxt arrays; the fix adds an in_parse_chunk_ re-entrancy guard that both queues re-entrant source into pending_src_ and CHECKs against nested entry.

Attack Path

  1. Feed XML that triggers a callback The attacker page supplies XML to the parser such that parsing reaches a SAX callback (e.g. during end-tag processing in xmlParseEndTag2) while inside DoWrite’s xmlParseChunk.
  2. Re-enter the parser from the callback The callback runs Blink logic that causes more source to be delivered to XMLDocumentParser::Append during the in-progress chunk parse. (inferred)
  3. Nested chunk parse reallocates `ctxt` arrays Pre-patch, because parser_paused_ is false, Append does not queue the source, so a nested xmlParseChunk runs and xmlRealloc()s ctxt->pushTab/nsTab, invalidating pointers the outer xmlParseEndTag2 still holds.
  4. Use-after-free on the outer frame Control returns to the outer parse, which dereferences the now-stale raw pointers into the reallocated arrays, causing a use-after-free / heap corruption. Any further exploitation primitive is not shown by the diff. (inferred)

Impact Assessment

An attacker serving crafted XML that induces re-entrant parsing can corrupt the libxml2 parser context’s heap buffers in the Blink renderer process, a use-after-free in Script/XML parsing that may be leveraged toward memory corruption. The advisory classifies this as a use-after-free with no exploited-in-the-wild status stated; the specific re-entrancy trigger and any escalation to a read/write primitive or code execution are (inferred) beyond what the guard and comments establish.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/xml/parser/xml_document_parser.cc
modified

Files Changed

  • third_party/blink/renderer/core/xml/parser/xml_document_parser.h
  • third_party/blink/renderer/core/xml/parser/xml_document_parser.cc

Audit Directions

  • Non-re-entrant C library callbacks
    Audit any Blink wrapper around a third-party library (libxml2, and similar) whose callbacks can synchronously re-enter Blink; nested calls that reallocate the library’s internal state are a recurring UAF source.
  • Guards that only cover one re-entrancy state
    Look for input-queuing conditions gated on a single flag (like parser_paused_) that fail to account for other in-progress states; a callback firing mid-operation may bypass the intended queue.
  • Raw pointers held across callbacks
    Search for code that captures pointers into growable buffers (*Tab, vectors, arenas) and continues using them after invoking user/SAX callbacks that could reallocate those buffers.
From 506527a4327dfaac8f02a520d0a823ca37a17690 Mon Sep 17 00:00:00 2001
From: Emmanuele Bassi <ebassi@igalia.com>
Date: Thu, 09 Jul 2026 04:09:31 -0700
Subject: [PATCH] Protect ParseChunk against re-entrancy

The chunk parser in libxml2 is not safe from re-entrancy: the parser
context contains multiple arrays that can be reallocated if a callback
happens to be invoked in the middle of an xmlParseChunk().

Any fix inside libxml2 would require a fair amount of work to make the
whole parser safe, so the easiest solution is to add a guard in the
calling code.

Bug: 501892500
Change-Id: I814c0f2101b8f8a53808bb9184fc119b0f5044d3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902291
Commit-Queue: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1659452}
---
diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc b/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc
index ece86d8..48288e2 100644
--- a/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc
+++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc
@@ -419,7 +419,7 @@
   if (IsStopped() || saw_xsl_transform_)
     return;
 
-  if (parser_paused_) {
+  if (parser_paused_ || in_parse_chunk_) {
     pending_src_.Append(source);
     return;
   }
@@ -970,6 +970,13 @@
   // Protect the libxml context from deletion during a callback
   scoped_refptr<XMLParserContext> context = context_;
 
+  // libxml2's push parser is not re-entrant: xmlParseEndTag2 holds multiple
+  // raw pointers inside ctxt, and a nested xmlParseChunk can xmlRealloc()
+  // those buffers. Crash safely rather than corrupt the heap. (Append()
+  // routes re-entrant data to pending_src_ so this should be unreachable.)
+  CHECK(!in_parse_chunk_);
+  base::AutoReset<bool> reentrancy_guard(&in_parse_chunk_, true);
+
   // libXML throws an error if you try to switch the encoding for an empty
   // string.
   if (parse_string.length()) {
diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser.h b/third_party/blink/renderer/core/xml/parser/xml_document_parser.h
index 6e09d417..b88e3d3 100644
--- a/third_party/blink/renderer/core/xml/parser/xml_document_parser.h
+++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser.h
@@ -221,6 +221,10 @@
   bool saw_first_element_;
   bool is_xhtml_document_;
   bool parser_paused_;
+  // Re-entrancy guard for DoWrite()/xmlParseChunk(). libxml2 push-parser
+  // contexts are not re-entrant; calling xmlParseChunk while already inside
+  // a SAX callback corrupts ctxt->pushTab/nsTab.
+  bool in_parse_chunk_ = false;
   bool requesting_script_;
   bool finish_called_;
   bool waiting_for_stylesheets_ = false;
Loading diff…

Original Bug Report

reported by vm...@google.com

Re-entrant Use-After-Free in libxml2 via XMLDocumentParser

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.

Overview: A re-entrancy vulnerability exists in Blink’s XML parser where synchronous script execution can lead to nested calls to libxml2’s xmlParseChunk on the same context. This allows an inner parse frame to reallocate internal buffers while an outer frame holds raw pointers to them. When the outer frame resumes, it accesses freed memory, potentially leading to an out-of-bounds write and remote code execution in the renderer.

Affected files:

  • third_party/blink/renderer/core/script/xml_parser_script_runner.cc
  • third_party/blink/renderer/core/xml/parser/xml_document_parser.cc
  • third_party/libxml/src/parser.c
  • third_party/blink/renderer/core/loader/document_loader.cc
  • third_party/blink/renderer/platform/loader/fetch/url_loader/navigation_body_loader.cc
  • third_party/blink/renderer/core/frame/local_frame.cc
  • third_party/blink/renderer/core/page/chrome_client.cc

Estimated timestamp from git blame: 2023-11-02

Summary

There is a potential Use-After-Free (UAF) and Out-Of-Bounds (OOB) write vulnerability in Blink’s integration with libxml2. The issue stems from the fact that XMLParserScriptRunner can execute scripts synchronously in a way that bypasses network-level re-entrancy guards. This allows an attacker to force a re-entrant call to xmlParseChunk on the same xmlParserCtxt. The inner parse can reallocate the parser’s namespace stack (pushTab), invalidating raw pointers held by the suspended outer parse frame.

Note: The following analysis is based on code inspection and traces. These are suggested/potential steps to trigger the issue, as our tooling does not yet have the ability to run a live proof-of-concept.

Technical Details

  1. Bypassing Re-entrancy Guards: When an external XML script finishes loading, XMLParserScriptRunner::PendingScriptFinished synchronously calls XMLDocumentParser::ResumeParsing(). Because this originates from a script loading callback rather than a NavigationBodyLoader network read, standard re-entrancy guards like is_in_on_readable_ and in_commit_data_ are false.
  2. Nested Message Loop: If the resumed parsing executes an inline script that calls alert(1), it triggers ChromeClient::OpenJavaScriptDialog, which creates a ScopedPagePauser. This spins a nested message loop.
  3. Synchronous Loader Resumption: When the user dismisses the dialog, the ScopedPagePauser destructor calls Page::SetPaused(false). This synchronously propagates down to NavigationBodyLoader::SetDefersLoading(kNone), which immediately calls OnReadable() to read pending Mojo data.
  4. Re-entrant Parse: Because the re-entrancy guards were false (as established in step 1), OnReadable successfully reads the next chunk of XML data and passes it to XMLDocumentParser::Append, leading to an inner, re-entrant call to xmlParseChunk on the identical xmlParserCtxt object.
  5. Use-After-Free in libxml2: The suspended outer parse frame is stopped inside xmlParseEndTag2 (third_party/libxml/src/parser.c), holding a raw const xmlStartTag *tag pointer to the ctxt->pushTab buffer. If the inner parse processes deeply nested namespace tags, nameNsPush calls xmlRealloc on ctxt->pushTab, freeing the memory the outer frame is pointing to.
  6. OOB Write: When the inner parse returns, the outer xmlParseEndTag2 resumes. It accesses tag->nsNr from the freed memory and passes it to xmlParserNsPop. If an attacker controls this memory and supplies a large positive integer for nsNr, it causes an integer underflow in xmlParserNsPop’s loop bounds. This leads to negative-index OOB reads and a subsequent OOB heap write (bucket->index = extra->oldIndex), providing a strong primitive for Remote Code Execution.

Potential Steps to Reproduce

An attacker could potentially trigger this by serving an application/xhtml+xml document using chunked transfer encoding:

  1. Send Chunk A: <script src="ext.js"></script>. This pauses the XML parser to wait for the external script.
  2. Send Chunk B: <a xmlns:p="u"><script>alert(1)</script>. This chunk is safely buffered in pending_src_ because the parser is paused.
  3. Send Chunk C: A payload of deeply nested <foo xmlns:a="1"> tags followed by an inline <script>groom()</script>. This sits unread in the Mojo data pipe.
  4. Finish ext.js: The browser finishes loading the external script. The callback resumes parsing and processes Chunk B.
  5. Trigger Pauser: The alert(1) in Chunk B executes, creating the ScopedPagePauser.
  6. Dismiss Dialog: When the dialog is dismissed, NavigationBodyLoader synchronously reads Chunk C, triggering the re-entrant inner parse.
  7. Reallocation and Grooming: The deeply nested tags in Chunk C force ctxt->pushTab to reallocate, freeing the old buffer. The groom() script allocates memory to reclaim the freed buffer and writes a controlled nsNr payload.
  8. Exploitation: The inner parse completes, the outer parse resumes, dereferences the dangling pointer to read the attacker’s nsNr, and executes the OOB write.

Suggested Fix

To prevent this vulnerability, XMLParserScriptRunner::PendingScriptFinished should avoid resuming the parser synchronously on the current call stack. It should instead use PostTask to schedule the resumption, ensuring the C++ stack is unwound and preventing cooperative re-entrancy. This approach is already successfully used by the HTML parser (HTMLParserScriptRunner::PendingScriptFinished).

Alternatively, XMLDocumentParser could maintain a strict internal re-entrancy boolean guard that CHECKs or safely drops nested calls to Append() or ResumeParsing() while a parse chunk is actively executing.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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