Medium watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebView
DescriptionUse after free in WebView
ComponentWebView
Sub-componentWebview
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker481920229
Fix commit34de88b274c4 (chromium/src) +28/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedAm4deu$
Reported2026-02-05 (per vendor advisory)
Fix landed2026-02-12 (public git)
Disclosed2026-03-10
Patch gap26 days — fix public in source before the advisory

Background

`JsCommunication`
A renderer-side, per-RenderFrame object in components/js_injection that holds the document-start/end scripts an embedder (e.g. Android WebView’s addDocumentStartJavaScript) registered and injects them into matching frames.
`RunScripts`
The JsCommunication method that, for a given mojom::DocumentInjectionTime, iterates over scripts_, checks each script’s origin_matcher against the frame’s security origin, and executes matching scripts in the main or an isolated world.
`base::WeakPtr`
A non-owning pointer that becomes null once the base::WeakPtrFactory that issued it is destroyed with its owner, so code can detect that an object is gone.
Re-entrancy via script execution
WebLocalFrame::ExecuteScript* runs JavaScript synchronously. That script can trigger frame detach or navigation, which can destroy the RenderFrame and its observers while the C++ caller is still on the stack.

Root Cause Analysis

Before the patch, JsCommunication::RunScripts looped over scripts_ and, for each matching entry, called render_frame()->GetWebFrame()->ExecuteScript(...) or ExecuteScriptInIsolatedWorld(...), then continued the loop using implicit this (both scripts_ and render_frame()). Running the injected script synchronously can destroy the JsCommunication object, as the patch’s added comment says (“executing a script may cause JsCommunication object to be destroyed”). The next loop iteration, or the range-for iterator advance over the now-destroyed scripts_, then used freed memory. The violated invariant is that this stays alive across a call into JavaScript.

The fix moves the loop into a static RunScriptsInternal that receives a base::WeakPtr<JsCommunication> from a new weak_ptr_factory_, reaches all members through that weak pointer, and checks if (!js_communication) return; right after each script execution, before the iterator advances or any member is touched again. RunScripts itself does nothing after the call, as noted by “Careful this may be destroyed.”

Key insight
The injection loop assumed JsCommunication survives synchronous execution of the scripts it injects, but that JavaScript can tear down the frame and its observer. The fix holds the object only through a WeakPtr and bails out as soon as it is invalidated after each execution.

Attack Path

  1. Embedder registers injected scripts A WebView-based app registers one or more document-start scripts (via JsCommunication) that match the attacker’s origin, or several scripts so the loop runs more than once.
  2. Victim loads attacker content The WebView navigates to attacker-controlled web content, and RunScripts fires at the document injection time.
  3. Script execution tears down the frame During ExecuteScript/ExecuteScriptInIsolatedWorld, JavaScript running in that frame (injected or reached via the page’s own handlers) causes the frame and its JsCommunication to be destroyed (inferred: for example by detaching the iframe synchronously).
  4. Loop resumes on freed object Control returns to the unpatched loop, which advances the iterator over scripts_ and calls render_frame() on the freed JsCommunication, causing a use-after-free in the renderer.

Impact Assessment

The bug is a use-after-free of a JsCommunication object in the renderer process. If exploited, an attacker could corrupt renderer memory and possibly run code inside the sandboxed renderer (inferred). Reachability depends on the embedder using the js_injection script-injection feature (Android WebView apps using document-start JavaScript, or other embedders of the component), on at least one registered script matching the frame’s origin, and on web content able to destroy the frame during injection. A stock Chrome browser tab that does not use this component is not shown to be affected. The vendor advisory rates it medium severity (“Use after free in WebView”); the advisory data provided does not report in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
for
components/js_injection/renderer/js_communication.cc
modified
if
components/js_injection/renderer/js_communication.cc
modified
JsObjectInfo
components/js_injection/renderer/js_communication.h
modified

Files Changed

  • components/js_injection/renderer/js_communication.cc
  • components/js_injection/renderer/js_communication.h

Audit Directions

  • Loops that call into script
    Look for loops over member containers that call ExecuteScript, ExecuteScriptInIsolatedWorld, CallFunction, event dispatch or other synchronous JS entry points without re-validating this (via WeakPtr or RenderFrameObserver lifetime) afterwards, especially in other RenderFrameObserver subclasses (extensions content scripts, autofill, translate).
  • Range-for over members during re-entrant calls
    Even with a liveness check, iterating a member std::vector while JavaScript can mutate it (e.g. via Mojo messages that add or remove scripts) can invalidate iterators. Audit scripts_-style containers for copy-before-iterate.
  • Code after re-entrant calls in observer callbacks
    In DidCreateDocumentElement, DidFinishDocumentLoad, RunScriptsAtDocumentStart-style hooks, check for any member access following a call that can run JavaScript or detach the frame.
From 34de88b274c41f245c28b123e03de9bc03e99163 Mon Sep 17 00:00:00 2001
From: Dave Tapuska <dtapuska@chromium.org>
Date: Thu, 12 Feb 2026 09:47:21 -0800
Subject: [PATCH] [webview] Fix an issue around lifecycle of WebLocalFrame

Ensure that we monitor destruction of the JsCommunication object.

Bug: 481920229
Change-Id: Ibbc6eacf0cb44fc737b0228c5d231d6fddf0caea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7567944
Commit-Queue: Dave Tapuska <dtapuska@chromium.org>
Reviewed-by: Bo Liu <boliu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1584064}
---

diff --git a/components/js_injection/renderer/js_communication.cc b/components/js_injection/renderer/js_communication.cc
index 4bfd8976..984f9f1 100644
--- a/components/js_injection/renderer/js_communication.cc
+++ b/components/js_injection/renderer/js_communication.cc
@@ -210,22 +210,38 @@
 }
 
 void JsCommunication::RunScripts(mojom::DocumentInjectionTime injection_time) {
-  url::Origin frame_origin =
-      url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin());
-  for (const auto& script : scripts_) {
+  RunScriptsInternal(weak_ptr_factory_.GetWeakPtr(), injection_time);
+  // Careful `this` may be destroyed.
+}
+
+// static
+void JsCommunication::RunScriptsInternal(
+    base::WeakPtr<JsCommunication> js_communication,
+    mojom::DocumentInjectionTime injection_time) {
+  CHECK(js_communication);
+  url::Origin frame_origin = url::Origin(
+      js_communication->render_frame()->GetWebFrame()->GetSecurityOrigin());
+  for (const auto& script : js_communication->scripts_) {
     if (!script->origin_matcher.Matches(frame_origin)) {
       continue;
     }
     if (script->injection_time == injection_time) {
       if (script->js_world == content::ISOLATED_WORLD_ID_GLOBAL) {
-        render_frame()->GetWebFrame()->ExecuteScript(
+        js_communication->render_frame()->GetWebFrame()->ExecuteScript(
             blink::WebScriptSource(script->script));
       } else {
-        render_frame()->GetWebFrame()->ExecuteScriptInIsolatedWorld(
-            script->js_world, blink::WebScriptSource(script->script),
-            blink::BackForwardCacheAware::kAllow);
+        js_communication->render_frame()
+            ->GetWebFrame()
+            ->ExecuteScriptInIsolatedWorld(
+                script->js_world, blink::WebScriptSource(script->script),
+                blink::BackForwardCacheAware::kAllow);
       }
     }
+    // Careful, executing a script may cause JsCommunication object to be
+    // destroyed.
+    if (!js_communication) {
+      return;
+    }
   }
 }
 
diff --git a/components/js_injection/renderer/js_communication.h b/components/js_injection/renderer/js_communication.h
index deb050f2..797305e 100644
--- a/components/js_injection/renderer/js_communication.h
+++ b/components/js_injection/renderer/js_communication.h
@@ -61,6 +61,10 @@
   class JsObjectInfo;
   struct JavaScriptExecutable;
 
+  static void RunScriptsInternal(
+      base::WeakPtr<JsCommunication> js_communication,
+      mojom::DocumentInjectionTime injection_time);
+
   void BindPendingReceiver(
       mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver);
 
@@ -80,6 +84,7 @@
   mojo::AssociatedRemote<mojom::JsObjectsClient> client_remote_;
 
   base::WeakPtrFactory<JsCommunication> weak_ptr_factory_for_bindings_{this};
+  base::WeakPtrFactory<JsCommunication> weak_ptr_factory_{this};
 };
 
 }  // namespace js_injection
Loading diff…

Original Bug Report

reported by po...@gmail.com

UAF in JsCommunication, leading to RCE

Steps to reproduce the problem

Tested on Pixel 9 Pro, Chromium version 144.0.7559.109.

  1. Build app.zip and Install
  2. python -m http.server
  3. Visit index.html
  4. UAF and Crash

Problem Description

JsCommunication is a content::RenderFrameObserver, which is destructed along with the destruction of the RenderFrameImpl [1]. On iOS and Android platforms, application developers can use interfaces (such as addDocumentStartJavaScript) to inject JavaScript code that runs at DocumentEnd or DocumentStart. JavaScript injected through this interface also runs in iframes. When the JavaScript executes, the scripts in the vector are processed one by one in a for-loop[2],[3].

void JsCommunication::OnDestruct() {
  delete this;              // [1]
}

void JsCommunication::RunScripts(mojom::DocumentInjectionTime injection_time) {
  url::Origin frame_origin =
      url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin());
  for (const auto& script : scripts_) {                  // [2]
    if (!script->origin_matcher.Matches(frame_origin)) {
      continue;
    }
    if (script->injection_time == injection_time) {
      if (script->js_world == content::ISOLATED_WORLD_ID_GLOBAL) {
        render_frame()->GetWebFrame()->ExecuteScript(
            blink::WebScriptSource(script->script));
      } else {
        render_frame()->GetWebFrame()->ExecuteScriptInIsolatedWorld(             // [3]
            script->js_world, blink::WebScriptSource(script->script),
            blink::BackForwardCacheAware::kAllow);
      }
    }
  }
}

The injected JavaScript can easily be redirected to code controlled by an attacker, for example, through getter, setter, or redefining functions in the prototype. An attacker can then remove the iframe, causing the RenderFrameImpl to be destroyed, which in turn destroys the JsCommunication object. When the next loop attempts to execute JavaScript, a UAF occurs, which is not protected by miraclePtr Although triggering this UAF requires certain preconditions, in practice, many browsers (such as the Pawxy browser) utilize this interface, and it is also widely used by third-party libraries (e.g., react-native-webview).

  1. app.zip is a simple WebView-based browser that injects two JavaScript codes via the addDocumentStartJavaScript interface.
    private void injectStartUpScripts() {
        if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
            Set<String> allowedOriginRules = Collections.singleton("*");
            try {
                WebViewCompat.addDocumentStartJavaScript(
                        webView,
                        "console.log(`${new Number(1.1)}`);",
                        allowedOriginRules
                );
//                WebViewCompat.addDocumentStartJavaScript(
//                        webView,
//                        "console.log(global_value)",
//                        allowedOriginRules
//                );
                WebViewCompat.addDocumentStartJavaScript(
                        webView,
                        "console.log('Second Script Injected');",
                        allowedOriginRules
                );
            } catch (Exception e) {
                Log.e(TAG, "addDocumentStartJavaScript failed");
            }
        } else {
            Toast.makeText(this, "addDocumentStartJavaScript not supported", Toast.LENGTH_SHORT).show();
        }
    }
  1. Using this browser to visit the attacker’s webpage.
  2. The attacker’s webpage redefines Number.prototype.toString inside an iframe.
node.contentWindow.Number.prototype.toString = function () {
  node.remove();
};
  1. The first injected JavaScript executes and triggers the attacker’s code, which removes the iframe, ultimately leading to the destruction of JsCommunication.
  2. In the next iteration of the loop, a UAF occurs.

Summary

UAF in JsCommunication, leading to RCE

Custom Questions

Reporter credit:

Am4deu$

Additional Data

Category: Security
Chrome Channel: Stable
Regression: N/A \

View on issue tracker