CVE-2026-79195
Overview
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
ctxtbuffers holding raw pointers that libxml2 mayxmlRealloc()(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.
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
- 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 insideDoWrite’sxmlParseChunk. - Re-enter the parser from the callback
The callback runs Blink logic that causes more source to be delivered to
XMLDocumentParser::Appendduring the in-progress chunk parse. (inferred) - Nested chunk parse reallocates `ctxt` arrays
Pre-patch, because
parser_paused_is false,Appenddoes not queue the source, so a nestedxmlParseChunkruns andxmlRealloc()sctxt->pushTab/nsTab, invalidating pointers the outerxmlParseEndTag2still holds. - 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
(inferred) beyond what the guard and comments establish.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/xml/parser/xml_document_parser.cc |
modified |
Files Changed
third_party/blink/renderer/core/xml/parser/xml_document_parser.hthird_party/blink/renderer/core/xml/parser/xml_document_parser.cc
Audit Directions
- Non-re-entrant C library callbacksAudit 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 stateLook 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 callbacksSearch 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.
Patch
From 506527a4327dfaac8f02a520d0a823ca37a17690 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi <[email protected]> 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 <[email protected]> Reviewed-by: Dominik Röttsches <[email protected]> Reviewed-by: Daniel Cheng <[email protected]> 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;
Original Bug Report
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.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser.ccthird_party/libxml/src/parser.cthird_party/blink/renderer/core/loader/document_loader.ccthird_party/blink/renderer/platform/loader/fetch/url_loader/navigation_body_loader.ccthird_party/blink/renderer/core/frame/local_frame.ccthird_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
- Bypassing Re-entrancy Guards: When an external XML script finishes loading,
XMLParserScriptRunner::PendingScriptFinishedsynchronously callsXMLDocumentParser::ResumeParsing(). Because this originates from a script loading callback rather than aNavigationBodyLoadernetwork read, standard re-entrancy guards likeis_in_on_readable_andin_commit_data_arefalse. - Nested Message Loop: If the resumed parsing executes an inline script that calls
alert(1), it triggersChromeClient::OpenJavaScriptDialog, which creates aScopedPagePauser. This spins a nested message loop. - Synchronous Loader Resumption: When the user dismisses the dialog, the
ScopedPagePauserdestructor callsPage::SetPaused(false). This synchronously propagates down toNavigationBodyLoader::SetDefersLoading(kNone), which immediately callsOnReadable()to read pending Mojo data. - Re-entrant Parse: Because the re-entrancy guards were
false(as established in step 1),OnReadablesuccessfully reads the next chunk of XML data and passes it toXMLDocumentParser::Append, leading to an inner, re-entrant call toxmlParseChunkon the identicalxmlParserCtxtobject. - Use-After-Free in libxml2: The suspended outer parse frame is stopped inside
xmlParseEndTag2(third_party/libxml/src/parser.c), holding a rawconst xmlStartTag *tagpointer to thectxt->pushTabbuffer. If the inner parse processes deeply nested namespace tags,nameNsPushcallsxmlRealloconctxt->pushTab, freeing the memory the outer frame is pointing to. - OOB Write: When the inner parse returns, the outer
xmlParseEndTag2resumes. It accessestag->nsNrfrom the freed memory and passes it toxmlParserNsPop. If an attacker controls this memory and supplies a large positive integer fornsNr, it causes an integer underflow inxmlParserNsPop’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:
- Send Chunk A:
<script src="ext.js"></script>. This pauses the XML parser to wait for the external script. - Send Chunk B:
<a xmlns:p="u"><script>alert(1)</script>. This chunk is safely buffered inpending_src_because the parser is paused. - 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. - Finish
ext.js: The browser finishes loading the external script. The callback resumes parsing and processes Chunk B. - Trigger Pauser: The
alert(1)in Chunk B executes, creating theScopedPagePauser. - Dismiss Dialog: When the dialog is dismissed,
NavigationBodyLoadersynchronously reads Chunk C, triggering the re-entrant inner parse. - Reallocation and Grooming: The deeply nested tags in Chunk C force
ctxt->pushTabto reallocate, freeing the old buffer. Thegroom()script allocates memory to reclaim the freed buffer and writes a controllednsNrpayload. - 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.