CVE-2025-2136
Overview
Background
- DevTools DOM snapshot
- The
InspectorDOMSnapshotAgentwalks the document tree on request from a connected DevTools session and, viaLegacyDOMSnapshotAgent, serializes each node into a protocol response. - `origin_url_map_`
- A
WTF::HashMap<DOMNodeId, String>(originally aliased asOriginUrlMap) that maps a backend node id to the URL of the script that generated the node, owned byInspectorDOMSnapshotAgentand consulted while building the snapshot. - `base::WeakPtr`
- A non-owning smart pointer that automatically becomes null when the object its
base::WeakPtrFactorybelongs to is destroyed, so a holder can test validity before dereferencing. - Session detach
- When a DevTools session detaches, the agent’s per-session state (including the origin-url hashmap) is torn down, which can happen re-entrantly while a snapshot is in progress.
Root Cause Analysis
InspectorDOMSnapshotAgent::GetSnapshot constructed a LegacyDOMSnapshotAgent and handed it a raw pointer to the owned origin_url_map_ (origin_url_map_.get()), which LegacyDOMSnapshotAgent stored as a plain OriginUrlMap* and dereferenced throughout snapshot construction. Because the snapshot walk can yield or re-enter, a DevTools session detach occurring mid-snapshot destroys the hashmap while LegacyDOMSnapshotAgent still holds the now-dangling raw pointer, so the subsequent origin_url_map_->Contains(...), ->at(...), and ->find(...) calls dereference freed memory (use-after-free). The guard if (origin_url_map_ && ...) only tested for a null raw pointer, which remained non-null after the underlying object was freed, so it did not prevent the access.
The fix wraps the hashmap in a new OriginUrlMap struct that carries a base::WeakPtrFactory, passes a base::WeakPtr<OriginUrlMap> down to LegacyDOMSnapshotAgent, and accesses the data through origin_url_map_->map; when the map is destroyed on detach, the weak pointer becomes null, so the existing if (origin_url_map_ && ...) guard now correctly short-circuits instead of touching freed memory.
base::WeakPtr<OriginUrlMap> makes the guard actually reflect whether the map is still alive.Attack Path
- Attach a DevTools session
An attacker with the ability to drive DevTools (e.g. via the DevTools protocol against the inspected page) enables the DOM snapshot agent so that
origin_url_map_is populated. - Trigger a snapshot
Invoke the snapshot capture, entering
InspectorDOMSnapshotAgent::GetSnapshot, which builds aLegacyDOMSnapshotAgentholding a raw pointer into the owned hashmap. - Detach mid-snapshot
Cause the DevTools session to detach while the snapshot walk is still running, destroying the hashmap and leaving the stored
OriginUrlMap*dangling. - Use-after-free dereference
Continued snapshot construction calls
origin_url_map_->Contains(...)/->at(...)/->find(...)on the freed hashmap, producing a use-after-free access. - Post-free memory manipulation
Reclaiming the freed allocation between the destroy and the dereference to gain controlled read/write is an exploitation step the patch does not demonstrate
(inferred).
Impact Assessment
(inferred). The precondition is an active, attacker-influenced DevTools/inspection session that can be detached at the right moment.Changed Functions
| Function | Change | Notes |
|---|---|---|
InspectedFramesthird_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h |
modified | |
Nodethird_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h |
modified | |
PaintLayerthird_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h |
modified | |
dom_debugger_agent_third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.cc |
modified | |
Nodethird_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h |
modified | |
PaintLayerthird_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h |
modified |
Files Changed
third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.ccthird_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.hthird_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.ccthird_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h
Audit Directions
- Raw pointers into owned containers passed to STACK_ALLOCATED helpersAudit other inspector agents that hand a
.get()/raw pointer of astd::unique_ptr-owned member to a short-lived helper; a re-entrant detach or document teardown during iteration can free the owner. - Null-checks that cannot see destructionFlag
if (ptr && ...)guards on raw pointers whose lifetime is tied to session/document state; a null test does not detect a freed-but-non-null pointer, so preferbase::WeakPtrorMember<>handles. - Long-running serialization across yield pointsReview DevTools/protocol handlers that walk the DOM or accumulate state over many nodes for callbacks, task posting, or session detach that can invalidate state captured at the start of the walk.
Patch
From 4fa8d897f83ab2ee66dc3505409a6e44936d7e4c Mon Sep 17 00:00:00 2001 From: Philip Pfaffe <[email protected]> Date: Wed, 26 Feb 2025 09:16:42 -0800 Subject: [PATCH] Fix crash in DOM snapshot agent When taking as snapshot, the DOM snapshot agent holds a pointer to a hashmap on the stack. The hashmap is destroyed when the devtools session detaches, so if that happens while taking a snapshot we run into a crash. Fixed: 395032416 Change-Id: I4b784cb3105f6a760ca5b1d3dcbe1ea254b3ad3a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6297444 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]> Cr-Commit-Position: refs/heads/main@{#1425198} --- diff --git a/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.cc b/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.cc index 4207bc5..4b7b8c54 100644 --- a/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.cc +++ b/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.cc @@ -214,13 +214,13 @@ CharacterData* character_data) { String origin_url = GetOriginUrl(character_data); if (origin_url) - origin_url_map_->insert(character_data->GetDomNodeId(), origin_url); + origin_url_map_->map.insert(character_data->GetDomNodeId(), origin_url); } void InspectorDOMSnapshotAgent::DidInsertDOMNode(Node* node) { String origin_url = GetOriginUrl(node); if (origin_url) - origin_url_map_->insert(node->GetDomNodeId(), origin_url); + origin_url_map_->map.insert(node->GetDomNodeId(), origin_url); } void InspectorDOMSnapshotAgent::EnableAndReset() { @@ -264,8 +264,10 @@ Document* document = inspected_frames_->Root()->GetDocument(); if (!document) return protocol::Response::ServerError("Document is not available"); - LegacyDOMSnapshotAgent legacySupport(dom_debugger_agent_, - origin_url_map_.get()); + LegacyDOMSnapshotAgent legacySupport( + dom_debugger_agent_, origin_url_map_ + ? origin_url_map_->weak_ptr_factory.GetWeakPtr() + : base::WeakPtr<OriginUrlMap>()); return legacySupport.GetSnapshot( document, std::move(style_filter), std::move(include_event_listeners), std::move(include_paint_order), std::move(include_user_agent_shadow_tree), @@ -517,16 +519,17 @@ BuildArrayForElementAttributes(node)); BuildLayoutTreeNode(node->GetLayoutObject(), node, index, contrast); - if (origin_url_map_ && origin_url_map_->Contains(backend_node_id)) { - String origin_url = origin_url_map_->at(backend_node_id); + if (origin_url_map_ && origin_url_map_->map.Contains(backend_node_id)) { + String origin_url = origin_url_map_->map.at(backend_node_id); // In common cases, it is implicit that a child node would have the same // origin url as its parent, so no need to mark twice. if (!node->parentNode()) { SetRare(nodes->getOriginURL(nullptr), index, std::move(origin_url)); } else { DOMNodeId parent_id = node->parentNode()->GetDomNodeId(); - auto it = origin_url_map_->find(parent_id); - String parent_url = it != origin_url_map_->end() ? it->value : String(); + auto it = origin_url_map_->map.find(parent_id); + String parent_url = + it != origin_url_map_->map.end() ? it->value : String(); if (parent_url != origin_url) SetRare(nodes->getOriginURL(nullptr), index, std::move(origin_url)); } diff --git a/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h b/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h index 3c57106..8e1a146fa 100644 --- a/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h +++ b/third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h @@ -5,6 +5,7 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_INSPECTOR_INSPECTOR_DOM_SNAPSHOT_AGENT_H_ #define THIRD_PARTY_BLINK_RENDERER_CORE_INSPECTOR_INSPECTOR_DOM_SNAPSHOT_AGENT_H_ +#include "base/memory/weak_ptr.h" #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/css/css_property_names.h" #include "third_party/blink/renderer/core/inspector/inspector_base_agent.h" @@ -25,6 +26,7 @@ class InspectedFrames; class Node; class PaintLayer; +struct OriginUrlMap; class CORE_EXPORT InspectorDOMSnapshotAgent final : public InspectorBaseAgent<protocol::DOMSnapshot::Metainfo> { @@ -105,7 +107,6 @@ static void VisitPaintLayer(PaintLayer*, PaintOrderMap* paint_order_map); using CSSPropertyFilter = Vector<const CSSProperty*>; - using OriginUrlMap = WTF::HashMap<DOMNodeId, String>; // State of current snapshot. std::unique_ptr<protocol::Array<protocol::DOMSnapshot::DOMNode>> dom_nodes_; diff --git a/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.cc b/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.cc index 472959e..def78f98 100644 --- a/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.cc +++ b/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.cc @@ -97,8 +97,8 @@ LegacyDOMSnapshotAgent::LegacyDOMSnapshotAgent( InspectorDOMDebuggerAgent* dom_debugger_agent, - OriginUrlMap* origin_url_map) - : origin_url_map_(origin_url_map), + base::WeakPtr<OriginUrlMap> origin_url_map) + : origin_url_map_(std::move(origin_url_map)), dom_debugger_agent_(dom_debugger_agent) {} LegacyDOMSnapshotAgent::~LegacyDOMSnapshotAgent() = default; @@ -186,16 +186,18 @@ .setBackendNodeId(IdentifiersFactory::IntIdForNode(node)) .build(); if (origin_url_map_ && - origin_url_map_->Contains(owned_value->getBackendNodeId())) { - String origin_url = origin_url_map_->at(owned_value->getBackendNodeId()); + origin_url_map_->map.Contains(owned_value->getBackendNodeId())) { + String origin_url = + origin_url_map_->map.at(owned_value->getBackendNodeId()); // In common cases, it is implicit that a child node would have the same // origin url as its parent, so no need to mark twice. if (!node->parentNode()) { owned_value->setOriginURL(std::move(origin_url)); } else { DOMNodeId parent_id = node->parentNode()->GetDomNodeId(); - auto it = origin_url_map_->find(parent_id); - String parent_url = it != origin_url_map_->end() ? it->value : String(); + auto it = origin_url_map_->map.find(parent_id); + String parent_url = + it != origin_url_map_->map.end() ? it->value : String(); if (parent_url != origin_url) owned_value->setOriginURL(std::move(origin_url)); } diff --git a/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h b/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h index 13c764b1..96a2687 100644 --- a/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h +++ b/third_party/blink/renderer/core/inspector/legacy_dom_snapshot_agent.h @@ -22,12 +22,17 @@ class Node; class PaintLayer; +struct OriginUrlMap { + WTF::HashMap<DOMNodeId, String> map; + base::WeakPtrFactory<OriginUrlMap> weak_ptr_factory{this}; +}; + class CORE_EXPORT LegacyDOMSnapshotAgent { STACK_ALLOCATED(); public: - using OriginUrlMap = WTF::HashMap<DOMNodeId, String>; - LegacyDOMSnapshotAgent(InspectorDOMDebuggerAgent*, OriginUrlMap*); + LegacyDOMSnapshotAgent(InspectorDOMDebuggerAgent*, + base::WeakPtr<OriginUrlMap>); LegacyDOMSnapshotAgent(const LegacyDOMSnapshotAgent&) = delete; LegacyDOMSnapshotAgent& operator=(const LegacyDOMSnapshotAgent&) = delete; ~LegacyDOMSnapshotAgent(); @@ -98,7 +103,7 @@ PaintOrderMap* paint_order_map_ = nullptr; // Maps a backend node id to the url of the script (if any) that generates // the corresponding node. - OriginUrlMap* origin_url_map_; + base::WeakPtr<OriginUrlMap> origin_url_map_; using DocumentOrderMap = HeapHashMap<Member<Document>, int>; InspectorDOMDebuggerAgent* dom_debugger_agent_; };
Original Bug Report
heap-use-after-free in blink::LegacyDOMSnapshotAgent::VisitNode
VULNERABILITY DETAILS UAF in blink::LegacyDOMSnapshotAgent::VisitNode
VERSION Chrome Version: 134.0.6986.0(Developer Build) Operating System: Ubuntu
REPRODUCTION CASE
- put manifest.json/background.js into the extension_path
- run the command: ./chrome –user-data-dir=./noexist –no-sandbox –load-extension=“extension_path”
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION Type of crash: tab Crash State: see asan.log file
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Sakana.S