290dca899b867e5ce18db7e25ffa91b1840dbea7 [PEPC] Fix crash when a PermissionElement is made from a shutdown reference.
Triage note: Web-reachable via a saved contentDocument reference after iframe navigation (WPT named use-after-free-crash), but the fix is a null-guard on a torn-down context, so it manifests as a null-deref/DoS rather than confirmed exploitable corruption. PEPC is also feature-gated.
The bug at a glance
This is a renderer-crash (null-pointer dereference) reachable from ordinary web script, fixed with defensive null-checks and a task-runner source swap. Despite the shipped test being named iframe-use-after-free-crash.html and the input labeling it UAF, the actual mechanism the diff addresses is a NULL dereference: when an HTMLPermissionElement (the PEPC /geolocation element) is created through a saved reference to an already-shutdown (detached) document, GetExecutionContext() returns nullptr, and that nullptr was dereferenced during cache registration (CachedPermissionStatus::From(GetExecutionContext())) and in GetTaskRunner() (GetExecutionContext()->GetTaskRunner(…)). The result is a reliable, scriptable renderer crash. It is scored medium (cvss ~6.5) because it is remotely triggerable with a tiny amount of JS, affects availability of the renderer process, and touches the permission-element attack surface, but it is not memory corruption and there is no evidence of an exploitable use-after-free primitive here: the fix is null-guarding a detached-document path, not repairing a dangling-pointer reuse. I keep bugClass honest as NullDeref rather than UAF; the crash is denial-of-service class, appropriately watch-tier / patch-soon rather than patch-now, and not exploitable for RCE on this evidence.
The angle is the classic ‘saved reference to a shut-down document’ lifetime trap. contentDocument captured before an iframe navigates away stays a live, scriptable Document object whose ExecutionContext has been torn down. createElement + appendChild on that stale document drives element-insertion code (InsertedInto) that assumes a live ExecutionContext. PEPC’s permission element registers itself with a per-context permission-status cache and fetches a task runner from the execution context at insertion time, so both assumptions fire exactly on the detached-document path an attacker can force deterministically.
Root cause
Two dereferences of a possibly-null ExecutionContext in HTMLPermissionElement. First, in HTMLPermissionElement::InsertedInto (html_permission_element.cc ~line 414): on insertion, if the element is not yet cache-registered and has permission descriptors, it called CachedPermissionStatus::From(GetExecutionContext())->RegisterClient(this, …). When the element belongs to a shut-down document, GetExecutionContext() is nullptr and CachedPermissionStatus::From(nullptr) dereferences it, crashing. The fix adds && GetExecutionContext() to the guard so registration is skipped when there is no execution context. Second, in HTMLPermissionElement::GetTaskRunner() (~line 1289): it returned GetExecutionContext()->GetTaskRunner(TaskType::kInternalDefault), again dereferencing a null execution context on the detached path. The fix switches the source to GetDocument().GetTaskRunner(TaskType::kInternalDefault) — the Document is always available (even when detached its task runners resolve safely) whereas the ExecutionContext may be gone. The shipped WPT (iframe-use-after-free-crash.html) reproduces it: it saves iframe.contentDocument before navigating the iframe to a data: URL (shutting the original document down), then in onload uses the saved reference to createElement(‘geolocation’), sets watch/autolocate/accuracymode attributes, and appendChild’s it into the detached document’s body, driving the crashing insertion path.
Key code
html_permission_element.cc: guard cache registration on a live ExecutionContext, and take the task runner from the Document instead of the (possibly null) ExecutionContext
Node::InsertionNotificationRequest HTMLPermissionElement::InsertedInto(
ContainerNode& insertion_point) {
HTMLElement::InsertedInto(insertion_point);
- if (!is_cache_registered_ && !permission_descriptors_.empty()) {
+ if (!is_cache_registered_ && !permission_descriptors_.empty() &&
+ GetExecutionContext()) {
CachedPermissionStatus::From(GetExecutionContext())
->RegisterClient(this, permission_descriptors_);
is_cache_registered_ = true;
}
...
scoped_refptr<base::SingleThreadTaskRunner>
HTMLPermissionElement::GetTaskRunner() {
- return GetExecutionContext()->GetTaskRunner(TaskType::kInternalDefault);
+ return GetDocument().GetTaskRunner(TaskType::kInternalDefault);
}
Patch walkthrough
third_party/blink/renderer/core/html/html_permission_element.cc (InsertedInto)— Adds GetExecutionContext() to the registration guard. Previously any insertion of a permission element with descriptors attempted CachedPermissionStatus::From(GetExecutionContext()), which dereferences the pointer; on a detached document that pointer is null and the renderer crashes. Now registration is simply skipped when there is no execution context — correct because there is no live context to register a permission-status client against.third_party/blink/renderer/core/html/html_permission_element.cc (GetTaskRunner)— Changes the task-runner source from GetExecutionContext()->GetTaskRunner(…) to GetDocument().GetTaskRunner(…). The element always has an associated Document whose task runner can be obtained safely, whereas the ExecutionContext can be null after document shutdown. This removes the second null-dereference site and gives GetTaskRunner a non-crashing result on the detached path.third_party/blink/web_tests/external/wpt/html/semantics/permission-element/geolocation-element/iframe-use-after-free-crash.html— New WPT crash test (class=test-wait pattern). It captures a child iframe’s contentDocument, navigates the iframe to a data: URL to shut the original document down, then in onload uses the saved document reference to create a geolocation permission element with watch/autolocate/accuracymode attributes and appends it. The test passes if the renderer does not crash. This is the canonical regression repro and confirms the trigger is pure, unprivileged web content.
Background
PEPC / HTMLPermissionElement — The Page Embedded Permission Control feature exposes a element (and specialized forms like geolocation) that mediates permission prompts inline. It registers with a per-execution-context CachedPermissionStatus and uses execution-context task runners, so its lifetime is coupled to a live context.
ExecutionContext vs Document lifetime — A Document object can outlive its ExecutionContext: after navigation/detachment the Document remains a scriptable JS object while GetExecutionContext() begins returning null. Code that assumes a non-null execution context on any node operation is vulnerable on the detached path.
Saved contentDocument reference — iframe.contentDocument captured before the iframe navigates yields a handle to the now-detached document. createElement/appendChild through it still runs Blink insertion machinery, a well-worn way to reach detached-document code paths from script.
CachedPermissionStatus::From — A supplement lookup keyed on an ExecutionContext; called with a null context it dereferences null. The fix avoids calling it at all when the context is gone.
Vulnerability window
- reported — Tracked as crbug 476235673; the crash is triggered by creating a permission/geolocation element from a shutdown document reference.
- fixed — Committed 2026-01-28 by Ravjit Uppal (r1576086), reviewed by Joey Arhar, shipping a WPT crash regression test alongside the null-guards.
Proof of concept
A crash reproducer was shipped as the WPT iframe-use-after-free-crash.html; it demonstrates a renderer crash (denial of service), not memory corruption or code execution. The reproduction is: embed an about:blank iframe, save iframe.contentDocument into a variable, then set iframe.src to a data: URL so the original document is detached/shut down. In the iframe’s onload handler, use the saved document reference to create a geolocation permission element, set its watch/autolocate/accuracymode attributes, and append it to the saved document’s body. Pre-fix, appendChild drives HTMLPermissionElement::InsertedInto, which calls CachedPermissionStatus::From(GetExecutionContext()) on a null execution context (and GetTaskRunner would likewise deref null), crashing the renderer. Post-fix the registration is skipped and the task runner comes from the Document, so no crash occurs. The code below is the shipped test verbatim; no exploit beyond the crash is demonstrated or implied.
<!doctype html>
<html class=test-wait>
<head>
<meta charset="utf-8" />
<title>Geolocation creation should not crash after document is shutdown</title>
<link rel="help" href="https://github.com/WICG/PEPC/blob/main/explainer.md" />
</head>
<body>
<iframe id="target-frame" src="about:blank"></iframe>
<script>
const iframe = document.getElementById("target-frame");
// Save a reference to its contentDocument
const savedDoc = iframe.contentDocument;
iframe.onload = () => {
// Call createElement through that reference
let geolocation = savedDoc.createElement("geolocation");
geolocation.setAttribute("watch", "");
geolocation.setAttribute("autolocate", "");
geolocation.setAttribute("accuracymode", "precise");
savedDoc.body.appendChild(geolocation);
document.documentElement.classList.remove("test-wait");
};
// Navigate away the iframe to "shut down" said document
// We navigate the iframe to a new URL. This detaches the original document.
iframe.src = "data:text/html,<h1>Navigated Away</h1>";
</script>
</body>
</html>
Exploitation
- trigger — Any web page can trigger it: save a child frame’s contentDocument, detach the frame via navigation, then create+insert a permission element through the saved reference. Fully unprivileged, deterministic, cross-browser-testable via WPT.
- primitive — The primitive is a null-pointer dereference in the renderer, i.e. a controlled crash. There is no attacker-controlled pointer value and no reuse of freed memory in the patched path, so it does not on this evidence yield a read/write or type confusion.
- ceiling — Impact ceiling is renderer denial of service. A null-deref at a fixed, non-attacker-controlled address is not a memory-corruption stepping stone by itself. Escalation would require a distinct bug; the ‘use-after-free’ name in the test is a misnomer relative to the code the fix touches.
Detection & hunting
For defenders and detection engineers:
- renderer crash signature —
- PEPC on detached documents —
Audit directions
- HTMLPermissionElement whole lifecycle —
- ExecutionContext deref on node-insertion paths —
- Document vs ExecutionContext task-runner usage —