← Chrome — 2026-W35

fe5c369f217719e724782de8d29f29edc5f203a6  [M150] Protect ParseChunk against re-entrancy

severity medium class UAF CVSS 8.8 signal score 6.8 third_party/blink/renderer/core/ AI review PLAUSIBLE
Structural signals: security_assert (+3.0), area:third_party/blink/renderer/core/ (+2.5), surgical (+1.3)

The bug at a glance

The diff’s own comments say a nested xmlParseChunk() can xmlRealloc() ctxt->pushTab/nsTab buffers while xmlParseEndTag2 still holds raw pointers into them, which is heap use-after-free/corruption in the renderer. It was cherry-picked to an M150 release branch with a restricted bug, which fits a high-severity renderer memory-safety fix.

XML documents are parsed by libxml2 inside the renderer, so if web content can make Blink feed data to the parser while a SAX callback is still running, it gets dangling pointers into reallocated libxml2 arrays. That is a classic starting point for renderer code execution.

Root cause

XMLDocumentParser::DoWrite() drives libxml2’s push parser through xmlParseChunk(), and the parser calls back into Blink (SAX callbacks) in the middle of a chunk. If such a callback re-entered Append()/DoWrite() while not paused, a nested xmlParseChunk() ran on the same context and could reallocate ctxt->pushTab/nsTab, while the outer frame (e.g. xmlParseEndTag2) kept raw pointers into the freed buffers. The fix adds an in_parse_chunk_ flag: Append() queues data into pending_src_ while it is set, and DoWrite() CHECKs and sets it with an AutoReset guard.

Key code

Re-entrant data is deferred to pending_src_, and nested xmlParseChunk becomes a safe CHECK crash

-  if (parser_paused_) {
+  if (parser_paused_ || in_parse_chunk_) {
     pending_src_.Append(source);
     return;
   }
...
+  CHECK(!in_parse_chunk_);
+  base::AutoReset<bool> reentrancy_guard(&in_parse_chunk_, true);

Patch walkthrough

  • third_party/blink/renderer/core/xml/parser/xml_document_parser.cc — Append() now also defers input to pending_src_ while a chunk is being parsed. DoWrite() hard-CHECKs against re-entry and sets in_parse_chunk_ for the duration of the xmlParseChunk call, so libxml2 context buffers cannot be reallocated under the outer parse frame.
  • third_party/blink/renderer/core/xml/parser/xml_document_parser.h — Adds the bool in_parse_chunk_ member, with comments documenting that libxml2 push contexts are not re-entrant and that nesting corrupts ctxt->pushTab/nsTab.

Background

libxml2 push parser (xmlParseChunk) — An incremental parser API that keeps growable arrays (node, name, namespace and push stacks) in xmlParserCtxt and calls SAX callbacks while it parses.

XMLDocumentParser — Blink’s XML/XHTML/SVG document parser; its SAX callbacks build DOM and can run author-observable work while libxml2 is mid-parse (inferred).

pending_src_ — Blink’s buffer for input that arrives while the parser cannot consume it; it is drained when parsing resumes.

Vulnerability window

  1. Reachability — Ordinary web content in a stock build can serve XML/XHTML/SVG documents parsed by this code. The re-entry needs a SAX callback that synchronously leads back into Append() while the parser is not paused (inferred, for example via script- or DOM-driven writes into the parsing document).
  2. Trigger — During an outer xmlParseChunk, a callback causes a nested DoWrite/xmlParseChunk on the same context, which grows and reallocates ctxt->pushTab/nsTab.
  3. Primitive — The outer xmlParseEndTag2 frame keeps using raw pointers into the freed buffers, a heap use-after-free/write in the renderer (per the diff comments).
  4. Escalation — Heap corruption in the renderer could be turned into renderer code execution and then chained with a sandbox escape (inferred).

Proof of concept

Conceptually: load an XML-family document so that, during element processing, a parser callback synchronously appends more markup to the same document while the parser is not paused. The nested parse then grows libxml2’s namespace/push stacks while an end-tag parse is in progress. No code is provided.

Exploitation

  1. Re-entry — Find a SAX callback path that synchronously reaches XMLDocumentParser::Append() without pausing the parser (inferred).
  2. Reallocation — Make the nested chunk push enough names/namespaces that libxml2 reallocates ctxt->nsTab/pushTab (inferred).
  3. Use of stale pointers — The outer xmlParseEndTag2 reads or writes through stale pointers into the freed allocation, which an attacker would try to reclaim with controlled data (inferred).

Detection & hunting

For defenders and detection engineers:

  • CHECK(!in_parse_chunk_) crash reports in XMLDocumentParser::DoWrite — After the patch, any attempted re-entry crashes safely here; crash clusters with this signature show attempted triggers or new re-entry paths.
  • ASan heap-use-after-free in xmlParseEndTag2 / xmlParseChunk with XMLDocumentParser frames on the stack — On unpatched builds, fuzz XML documents with script-driven writes and look for this stack shape.

Audit directions

  • Other Blink callers of libxml2/libxslt push or SAX APIs (XSLT processing, XMLHttpRequest document parsing, DOMParser for XML) — Check whether any callback can synchronously re-enter xmlParseChunk or another API that mutates the same ctxt.
  • XMLDocumentParser callbacks that run script or fire events (script elements, processing instructions, custom element reactions) — Make sure every path that can run author code mid-chunk either pauses the parser or is now covered by in_parse_chunk_.

Before / after

Loading diff…