5549b3663c5d3904eab780389cd14c58523d1dfa Use-after-free of ScriptExecutionContext in trustedTypeCompliantString
Triage note: Attr::setValue wraps contextDocument() in an extra protect() before trustedTypesCompliantAttributeValue, which runs author-defined Trusted Types policy JS that can destroy the context document; previously the raw Document* could dangle across that reentrant call.
The bug at a glance
A use-after-free / dangling-pointer lifetime bug in Attr::setValue (Source/WebCore/dom/Attr.cpp) in the WebContent process. When setting an attribute that is subject to Trusted Types enforcement, the code passed the context Document to trustedTypesCompliantAttributeValue, which synchronously invokes an author-defined Trusted Types policy callback (JavaScript). That JS can navigate/detach frames and drop the last reference to the context document, leaving the raw Document* passed into the helper dangling for the remainder of the call. The diff proves a lifetime defect (the context document was not protected across reentrant script); high severity is reasonable for a reachable DOM UAF, but because the helper body is not in the diff, that the freed document is actually used after the callback — and any escalation to RCE — is (inferred).
A double-protect fix: the outer protect() guarded document(), but document()->contextDocument() can be a different Document, and only THAT one is handed into the Trusted-Types policy callback that can free it.
Root cause
The invariant is that any Document* live across a call that can run author JS must be held by a Ref/RefPtr. Attr::setValue, when trustedTypeForAttribute returns a non-null attributeType, called trustedTypesCompliantAttributeValue(protect(document())->contextDocument(), type.attributeType, value, type.sink). Here protect(document()) returns a Ref that keeps document() alive and then ->contextDocument() returns a raw Document* — the context document, which for an attribute node in an adopted/imported/template subtree is generally NOT the same object as document(). Only document() was ref-protected; contextDocument() was an unprotected raw pointer.
The pre-patch sequence is: 1. compute type = trustedTypeForAttribute(…); 2. form raw ptr ctx = protect(document())->contextDocument() (ctx not ref-held); 3. enter trustedTypesCompliantAttributeValue, which resolves the document’s Trusted Types policy and synchronously calls the author’s createHTML/ createScript/createScriptURL JS callback; 4. that callback runs arbitrary script and can detach the iframe/document that is ctx, dropping its last Ref so the Document is destroyed; 5. the helper continues to use ctx (to read the TrustedTypes registry / report violations / build the compliant value) on freed memory. Document is RefCounted (not Oilpan), so once the author script releases the last external Ref the object is freed immediately — there is no GC grace period.
The fix wraps the inner expression in a second protect(): trustedTypesCompliantAttributeValue(protect(protect(document())->contextDocument()), …). The inner protect(document()) keeps document() alive long enough to safely call contextDocument(); the outer protect() takes a Ref on the returned context Document, so it survives the entire reentrant policy callback. Bugclass matches the triage (UAF) at the lifetime level; whether the subsequent use is a read or a type-confused dispatch is not shown in the diff.
Key code
Attr::setValue protects the context document, not just document(), across the Trusted Types callback
- auto compliantValue = trustedTypesCompliantAttributeValue(protect(document())->contextDocument(), type.attributeType, value,
+ auto compliantValue = trustedTypesCompliantAttributeValue(protect(protect(document())->contextDocument()), type.attributeType, value,
type.sink);
if (compliantValue.hasException())
return compliantValue.releaseException();
Fix approach
The patch introduces a second holder: protect(protect(document())->contextDocument()) takes a Ref on the context Document itself (not merely on document()), so that Ref outlives the entire trustedTypesCompliantAttributeValue call including the synchronous author-policy JS it dispatches. The inner protect still guards document() for the duration of the contextDocument() accessor. The only semantic change is lifetime: the context document is now guaranteed alive until setValue returns, so even if the policy callback detaches and releases that document, the object is not destroyed until the extra Ref drops after the helper finishes. No behavior changes on the non-adversarial path.
Patch walkthrough
Source/WebCore/dom/Attr.cpp— In Attr::setValue, the context document passed to trustedTypesCompliantAttributeValue is now wrapped in an extra protect(), turning the previously raw Document* (that could be freed by the Trusted Types policy callback) into a ref-held Document for the duration of the reentrant call.
Background
Trusted Types — A web platform defense that routes risky string sinks (innerHTML, attribute values, script URLs) through author-registered policy objects. Each policy’s createHTML/createScript/createScriptURL is arbitrary JS invoked synchronously when the sink is written, giving attacker-or-author script a reentrancy window inside native DOM code.
contextDocument() — A Document accessor returning the document that owns the current scripting context, which can differ from the node’s own document() for imported/adopted nodes, template content documents, or iframes — so protecting document() does not protect contextDocument().
protect() / Ref / RefPtr — WebCore’s RAII reference holders. protect(x) returns a Ref that increments x’s refcount for the scope; passing the raw result of a method call (not wrapped) keeps no reference. WebCore Documents are RefCounted, so the last-Ref drop frees them synchronously.
Attr::setValue — Implements setting an Attr node’s value (e.g. attr.value = …). For attributes mapped to a Trusted Types sink (via trustedTypeForAttribute) it must obtain a compliant value before committing, which is where the policy callback fires.
Reentrancy / synchronous callout — A native DOM operation that synchronously calls into JS can have its own operands freed by that JS before control returns; every raw pointer live across such a callout must be ref-pinned first.
Vulnerability window
- Setup — Page registers a Trusted Types policy whose attribute callback runs attacker JS; it arranges a node whose contextDocument() is a detachable document (e.g. an iframe’s document).
- Trigger — Script sets a Trusted-Types-guarded attribute value on an Attr node, entering Attr::setValue and then trustedTypesCompliantAttributeValue with the raw context Document*.
- Reentry — The policy callback executes and detaches/removes the frame or otherwise drops the last reference to the context document.
- Free — The RefCounted Document is destroyed immediately while trustedTypesCompliantAttributeValue still holds the raw pointer.
- Use-after-free — Continued use of the freed context document inside the helper (registry lookup / violation reporting) touches freed memory (inferred — helper body not in diff).
Proof of concept
No regression test ships in the diff, so this is conceptual (web path inferred). Conceptual trigger: (1) in a top document, create an and obtain a node whose contextDocument() is the iframe’s document; (2) register a Trusted Types policy via trustedTypes.createPolicy whose relevant attribute callback synchronously removes that iframe (frame.remove()) so the iframe document loses its last reference; (3) set a Trusted-Types-enforced attribute on the node (triggering Attr::setValue) so the callback fires mid-call. Pre-patch the raw contextDocument() pointer dangles for the rest of the call.
Exploitation
- Reach — Pure JS in a page that enables Trusted Types (or the default policy); no special permissions.
- Trigger — A Trusted-Types attribute write whose policy callback frees the context document.
- Primary primitive — Dangling Document* used after free inside trustedTypesCompliantAttributeValue — a UAF read at minimum.
- Secondary — If the helper calls a virtual on the freed Document or re-enters its TrustedTypes registry, a reclaimed allocation could steer that access; Document is a large, frequently-allocated object, aiding grooming.
- Escalation (open) — Gating questions: exactly what the helper does with the document after the callback (read vs virtual dispatch), and whether the freed slot can be reclaimed with controlled data within the single synchronous window — not determinable from the diff (inferred).
Detection & hunting
For defenders and detection engineers:
- Document destroyed during Trusted Types callback —
- ASan UAF in Attr::setValue path —
- Frame detach inside sink enforcement —
Remaining risk in the patch
- Helper not in diff — trustedTypesCompliantAttributeValue’s body is not supplied; the fix protects the document it receives, but any OTHER raw pointer that helper holds across the callback (e.g. the element, or type.sink-derived state) is unverified from this diff.
- Pattern, not systemic guard — The fix hardens one call site by adding a protect(); it does not add a general invariant preventing future Trusted-Types sink callers from passing unprotected context documents.
Audit directions
- Other Trusted Types sink callers —
- contextDocument() raw uses —
- Reentrancy via author callbacks —
- Adopted/imported/template documents —