CVE-2026-11649
Overview
Background
- `v8-deep-serializer.cc`
- the Inspector component that deeply serializes JavaScript values into the protocol’s structured
SerializedValueform (used by the BiDi/Runtimevalue-serialization path). - `Response`
- the Inspector result type that carries either success or an error;
response.IsSuccess()distinguishes the two, and a non-successResponsemeans the serialization did not complete normally. - `serializedValue`
- the out-parameter (
&serializedValue) the serialization helper fills in; on failure it may be left unset, incomplete, or in a moved-from/invalid state. - `result.setValue("value", std::move(serializedValue))`
- moves the serialized value into the outgoing result object, consuming
serializedValue.
Root Cause Analysis
In the serialization routine (the enclosing function is not shown in the @@ context, so it is not named here), the code invoked the deep-serialization helper with &serializedValue and captured its Response into response, but then unconditionally executed result.setValue("value", std::move(serializedValue)) and returned Response::Success() without ever inspecting response. When the helper returned an error, serializedValue had not been validly populated, yet it was still moved into result and reported as success. The violated invariant is that a failed Response must be propagated and its incompletely-produced output must not be consumed. Consuming/serializing a value that a failed deep-serialization left in an invalid state is what the advisory attributes to a use-after-free, i.e. the error path had left a dangling or invalidated reference that the subsequent setValue then used (inferred, since the freeing site is not in this hunk).
The fix inserts if (!response.IsSuccess()) return response; immediately after the serialization call and before result.setValue, so on any serialization error the function returns the error early and never touches the invalid serializedValue.
Response from deep serialization and consuming serializedValue even on failure; the fix adds an early if (!response.IsSuccess()) return response; so a failed serialization is propagated instead of letting the invalid result be used.Attack Path
- Request deep value serialization
The attacker drives the Inspector/BiDi path (e.g. a
Runtimecommand that deep-serializes a value) so this serialization routine runs on an attacker-chosen object. - Force a serialization failure
By supplying a value/structure that makes the deep-serialization helper return a non-success
Response(leavingserializedValueinvalid), the attacker reaches the unguarded error path. - Trigger use of the invalid value
Before the fix, execution falls through to
result.setValue("value", std::move(serializedValue)), consuming the incompletely-produced / invalidatedserializedValue, which the advisory attributes to a use-after-free (the specific freed object is not shown in the diff, inferred). - Escalate via reclaimed memory Reallocating the freed backing memory with controlled data could convert the dangling use into a corruption primitive (inferred); the patch demonstrates only the missing error check.
Impact Assessment
Files Changed
src/inspector/v8-deep-serializer.cc
Audit Directions
- Ignored `Response`/status returnsSearch for sites that capture a
Response/status from a fallible call but then use the out-parameter and return success without checkingIsSuccess(). - Out-parameter use on failureAudit serializer/converter helpers that fill a
&outvalue, ensuring callers do notstd::move/consume that out-parameter when the call reported an error. - Deep/recursive serialization pathsReview other
V8DeepSerializer/V8ValueSerializerrecursion points where a nested failure must abort the whole serialization rather than proceeding with partially-built results.
Patch
From bb96b8e67ac4a4482ce812ff27d8ec3e4a36116c Mon Sep 17 00:00:00 2001
From: Philip Pfaffe <pfaffe@chromium.org>
Date: Wed, 13 May 2026 11:13:38 +0000
Subject: [PATCH] [inspector] Return error response in value serialization
Fixed: 511270083
Change-Id: I673fc070de370a4ca0ec4d7fc52dc9fa6dc0f156
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7843197
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Auto-Submit: Philip Pfaffe <pfaffe@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107299}
---
diff --git a/src/inspector/v8-deep-serializer.cc b/src/inspector/v8-deep-serializer.cc
index 2bb04a6..cf83fb9 100644
--- a/src/inspector/v8-deep-serializer.cc
+++ b/src/inspector/v8-deep-serializer.cc
@@ -205,6 +205,7 @@
additionalParameters,
duplicateTracker, &serializedValue);
+ if (!response.IsSuccess()) return response;
result.setValue("value", std::move(serializedValue));
}
return Response::Success();
Original Bug Report
UAF write in V8 Inspector deep serialization via ignored SerializeSet error
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) exists in the V8 inspector’s deep serialization when V8DeepSerializer::SerializeSet ignores errors. This leaves raw, dangling pointers in the duplicate tracker, which can be exploited for arbitrary memory corruption and remote code execution if the freed memory is reclaimed.
Affected files:
v8/src/inspector/v8-deep-serializer.ccv8/src/inspector/v8-serialization-duplicate-tracker.ccv8/src/inspector/value-mirror.ccthird_party/blink/renderer/core/inspector/thread_debugger_common_impl.cc
Estimated timestamp from git blame: 2023-06-19
Potential Consequence: Exploitation yields a highly controllable Use-After-Free (UAF) write primitive, potentially leading to arbitrary memory corruption and Remote Code Execution (RCE) in the renderer process.
Technical Summary:
Initial logic and parameters are validated: an attacker can trigger deep serialization via CDP (Runtime.evaluate) on a crafted structure containing a Set and a custom JS getter. The V8SerializationDuplicateTracker accurately registers object references using v8::External mappings (circumventing MiraclePtr protections).
Standard processing applies during serialization, where a deeply nested DOM tree legitimately exhausts maxDepth, forcing a Response::ServerError and the destruction of the local ListValue containing previously processed Set elements.
The vulnerability triggers when V8DeepSerializer::SerializeSet unconditionally ignores this error state and returns Response::Success(). This bypass allows the outer serialization routine to immediately invoke the attacker-controlled JS getter. The getter performs targeted heap reclamation over the freed DictionaryValue memory and returns the original Set object. The V8SerializationDuplicateTracker blindly fetches the now-dangling raw pointer, executing setInteger operations directly on attacker-controlled std::unordered_map structures, finalizing the UAF write.
Potential Steps to Reproduce: (Note: These are potential steps as our tooling agent cannot currently execute code).
- Construct an
Arraywhere index 0 is aSetcontaining a target object and a deeply nested DOM tree. - Assign a custom JS getter to index 1 of the
Array. - Trigger deep serialization via CDP (
Runtime.evaluatewithserializationOptions: {serialization: "deep"}). - The deeply nested DOM tree causes an intentional serialization failure, freeing the target object’s
DictionaryValue. SerializeSetignores the error, advancing to index 1 and invoking the JS getter.- The JS getter reclaims the freed memory with payload data and returns the target object, forcing a UAF write via the duplicate tracker.
Suggested Fix:
Update V8DeepSerializer::SerializeSet in v8/src/inspector/v8-deep-serializer.cc to explicitly check and return the Response from SerializeArrayValue:
Response response = SerializeArrayValue(value->AsArray(), context, maxDepth, additionalParameters, duplicateTracker, &serializedValue);
if (!response.IsSuccess()) return response;
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.