CVE-2025-13721
Overview
Background
- `Map`
- V8’s hidden-class object describing an object’s layout, including its instance size and in-object property count, and which can be updated concurrently (e.g. by transitions or the GC/compiler threads).
- Embedder fields
- extra tagged slots reserved after the object header of certain
JSObjects for use by the embedder (e.g. Blink), whose count is derived from the map’s instance size minus header and in-object property space. - `instance_size` vs `instance_size_in_words`
- the map stores its instance size once as a word count (
instance_size_in_words()), from which the byte size is computed;instance_size()andGetInObjectProperties()each independently read that stored field. - `kVariableSizeSentinel`
- the sentinel value in the instance-size field marking variable-sized objects, for which the embedder-field count is defined to be 0.
Root Cause Analysis
JSObject::GetEmbedderFieldCount in src/objects/js-objects-inl.h computed its result by reading the map’s instance-size field more than once: once via map->instance_size() and again indirectly via map->GetInObjectProperties() (which itself derives from the instance size). If the map was concurrently updated between these reads, the two values could reflect different instance sizes, so the subtraction (((instance_size - GetEmbedderFieldsStartOffset(map)) >> kTaggedSizeLog2) - map->GetInObjectProperties()) could be computed from mutually inconsistent inputs, yielding a wrong (potentially too-large or negative) embedder-field count.
The fix reads the size field exactly once into instance_size_in_words, derives instance_size from it (instance_size_in_words << kTaggedSizeLog2), and computes the in-object property count as instance_size_in_words - map->GetInObjectPropertiesStartInWords() from that same snapshot, so both terms of the subtraction come from a single consistent read. This restores the invariant that the count is computed from one coherent view of the map, eliminating the torn-read race.
instance_size_in_words() once and derives every dependent quantity from that one value.Attack Path
- Set up an object with embedder fields
Obtain a
JSObjectwhose map participates in a code path that callsGetEmbedderFieldCountto size or bound embedder-slot access. - Race a map update
Cause the object’s map to be updated concurrently (e.g. via a shape transition or background compilation) so the instance-size field changes while
GetEmbedderFieldCountruns. - Win the torn read Have the function read the old instance size for one term and the new value for the other, producing an inconsistent embedder-field count.
- Miscount embedder fields The wrong count is returned to callers that use it to iterate or bound embedder slots, leading to an out-of-range field index (inferred: the concrete OOB access sink is a caller and is not shown in this diff).
Impact Assessment
GetEmbedderFieldCount return an inconsistent count, and callers that trust it may read or write embedder slots out of the object’s true bounds; any resulting OOB access or stronger primitive is (inferred) since the patch shows only the inconsistent computation, not a sink. The advisory classifies this as a race in V8; it does not state exploited-in-the-wild status. Preconditions are the ability to run script and to cause a concurrent map mutation that races the count computation.Files Changed
src/objects/js-objects-inl.h
Audit Directions
- Repeated field reads under concurrencySearch inline/
-inl.hhelpers that read the sameMapfield (instance size, in-object properties, bit fields) more than once within a computation; snapshot such fields into a local once. - Derived-quantity consistencyAudit places that combine
instance_size(),GetInObjectProperties(), and header offsets, ensuring all terms come from one coherent read when the map may be mutated concurrently. - Count/size results feeding boundsTrace callers of size/count accessors that bound slot iteration (embedder fields, in-object properties) and confirm the source values cannot be torn by background threads.
Patch
From b8b01791845cb77d5482f299310e9bfdcd012842 Mon Sep 17 00:00:00 2001
From: Nikolaos Papaspyrou <nikolaos@chromium.org>
Date: Tue, 21 Oct 2025 13:31:10 +0200
Subject: [PATCH] [objects] Avoid race in JSObject::GetEmbedderFieldCount
This CL fixes JSObject::GetEmbedderFieldCount, so that it only reads
the map's instance size field once. This avoids calculating an
inconsistent result, in case the map is concurrently updated.
Bug: 355120682
Change-Id: Ia6e003896998227b5b5bd88cc480b4bc1adbf9f2
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7066814
Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
Commit-Queue: Nikolaos Papaspyrou <nikolaos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#103247}
---
diff --git a/src/objects/js-objects-inl.h b/src/objects/js-objects-inl.h
index f439860..e2d85ab 100644
--- a/src/objects/js-objects-inl.h
+++ b/src/objects/js-objects-inl.h
@@ -352,16 +352,22 @@
// static
int JSObject::GetEmbedderFieldCount(Tagged<Map> map) {
- int instance_size = map->instance_size();
+ // We inline some code from Map::instance_size and Map::GetInObjectProperties
+ // here, to avoid reading the map's instance size field twice.
+ // See https://crbug.com/355120682.
+ int instance_size_in_words = map->instance_size_in_words();
+ int instance_size = instance_size_in_words << kTaggedSizeLog2;
if (instance_size == kVariableSizeSentinel) return 0;
// Embedder fields are located after the object header, whereas in-object
// properties are located at the end of the object. We don't have to round up
// the header size here because division by kEmbedderDataSlotSizeInTaggedSlots
// will swallow potential padding in case of (kTaggedSize !=
// kSystemPointerSize) anyway.
+ int in_object_properties =
+ instance_size_in_words - map->GetInObjectPropertiesStartInWords();
return (((instance_size - GetEmbedderFieldsStartOffset(map)) >>
kTaggedSizeLog2) -
- map->GetInObjectProperties()) /
+ in_object_properties) /
kEmbedderDataSlotSizeInTaggedSlots;
}
Original Bug Report
cctest/test-inobject-slack-tracking/SubclassTypedArrayBuiltinNoInlineNew starts flaking
Failing test: cctest/test-inobject-slack-tracking/SubclassTypedArrayBuiltinNoInlineNew Failure link: https://cr-buildbucket.appspot.com/build/8741558669677733889 Link to Flako run: http://ci.chromium.org/b/8741554664431477521/infra Suspected commit: https://chromium-review.googlesource.com/c/v8/v8/+/5696786
Crash type: DCHECK failure
Crash state: JSObject::MayHaveEmbedderFields(map) implies UncheckedCast<JSObject>(obj)->GetEm
Error summary:
Fatal error in ../../src/objects/objects-body-descriptors-inl.h, line 109
Debug check failed: JSObject::MayHaveEmbedderFields(map) implies UncheckedCast<JSObject>(obj)->GetEmbedderFieldCount() == 0.
#FailureMessage Object: 0x16b84a738 ==== C stack trace ===============================
0 libv8_libbase.dylib 0x0000000105f01148 v8::base::debug::StackTrace::StackTrace() + 24
1 libv8_libplatform.dylib 0x0000000105f44f28 v8::platform::(anonymous namespace)::PrintStackTrace() + 116
2 libv8_libbase.dylib 0x0000000105ee4930 V8_Fatal(char const*, int, char const*, ...) + 352
3 libv8_libbase.dylib 0x0000000105ee4264 v8::base::SetFatalFunction(void (*)(char const*, int, char const*)) + 0
4 libv8_for_testing.dylib 0x000000010e324434 void v8::internal::BodyDescriptorBase::IterateJSObjectBodyWithoutEmbedderFieldsImpl<v8::internal::ConcurrentMarkingVisitor>(v8::internal::Tagged<v8::internal::Map>, v8::internal::Tagged<v8::internal::HeapObject>, int, int, v8::internal::ConcurrentMarkingVisitor*) + 304
5 libv8_for_testing.dylib 0x000000010e326194 int v8::internal::HeapVisitor<int, v8::internal::ConcurrentMarkingVisitor>::VisitJSObjectSubclass<v8::internal::JSTypedArray, v8::internal::JSTypedArray::BodyDescriptor>(v8::internal::Tagged<v8::internal::Map>, v8::internal::Tagged<v8::internal::JSTypedArray>) + 136
6 libv8_for_testing.dylib 0x000000010e306870 v8::internal::ConcurrentMarking::RunMajor(v8::JobDelegate*, v8::base::EnumSet<v8::internal::CodeFlushMode, int>, unsigned int, bool) + 988
7 libv8_for_testing.dylib 0x000000010e366940 v8::internal::ConcurrentMarking::JobTaskMajor::Run(v8::JobDelegate*) + 480
8 libv8_libplatform.dylib 0x0000000105f43bd8 v8::platform::DefaultJobWorker::Run() + 260
Crash analysis hash: 3bdc04fbe0e20d5d823e093abdd16035