Chrome · V8
CVE-2026-10904
Logic Error in V8
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/builtins/builtins-dataview.cctest/mjsunit/regress/regress-506855825.js
Patch
From 33395e4fe24b9dc5b435db658d992f68c758af43 Mon Sep 17 00:00:00 2001
From: Jakob Linke <jgruber@chromium.org>
Date: Thu, 30 Apr 2026 09:32:00 +0200
Subject: [PATCH] [array-buffer] Defer AttachView in DataView ctor past detach check
The DataView constructor registered the new view on the underlying
ArrayBuffer (via AttachView) before the second IsDetachedBuffer
check. If the byteLength arg's valueOf (or new.target.prototype's
getter) detached the buffer, AttachView ran on a detached buffer
and the orphan view leaked into its `views` field even though the
constructor threw, violating the tracking invariant verified by
--verify-heap.
Move AttachView to immediately after the second IsDetachedBuffer
check. The remaining constructor steps (ArrayBufferByteLength read,
RAB offset/length range checks) cannot run user code, so the buffer
cannot become detached past that point.
AttachView must still run before those RAB range checks: if we
deferred it further and one of them threw on a resized-but-not-
detached buffer, the orphan view would have `views == kNoView`
while WasDetached() is false, which violates
CheckArrayBufferViewTrackingConsistency (see test262
DataView/custom-proto-access-resizes-buffer-*).
Fixed: 506855825
Change-Id: Ib65499805b5e75b09c999e3c0e07dfac49f575d9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7806344
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Reviewed-by: Olivier Flückiger <olivf@chromium.org>
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106956}
---
diff --git a/src/builtins/builtins-dataview.cc b/src/builtins/builtins-dataview.cc
index 4c0a7cd..952845d 100644
--- a/src/builtins/builtins-dataview.cc
+++ b/src/builtins/builtins-dataview.cc
@@ -143,8 +143,6 @@
raw->set_buffer(*array_buffer);
}
- array_buffer->AttachView(*data_view);
-
// 13. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if (array_buffer->was_detached()) {
THROW_NEW_ERROR_RETURN_FAILURE(
@@ -154,6 +152,13 @@
isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
}
+ // AttachView only after the detach check, so a detached buffer does not end
+ // up tracking a view that is never returned to script (the orphan view's
+ // WasDetached() would still be true, but the buffer's views list would be
+ // inconsistent with its detached state). The remaining steps below cannot
+ // detach the buffer, so attaching here is safe.
+ array_buffer->AttachView(*data_view);
+
// 14. Let getBufferByteLength be
// MakeIdempotentArrayBufferByteLengthGetter(SeqCst).
// 15. Set bufferByteLength be getBufferByteLength(buffer).
diff --git a/test/mjsunit/regress/regress-506855825.js b/test/mjsunit/regress/regress-506855825.js
new file mode 100644
index 0000000..4e4f87f
--- /dev/null
+++ b/test/mjsunit/regress/regress-506855825.js
@@ -0,0 +1,11 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --verify-heap --track-array-buffer-views
+
+const ab = new ArrayBuffer(64);
+new Uint8Array(ab);
+
+const evil = { valueOf() { ab.transfer(); return 3; } };
+assertThrows(() => new DataView(ab, 0, evil), TypeError);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/regress/regress-506855825.js b/test/mjsunit/regress/regress-506855825.js
new file mode 100644
index 0000000..4e4f87f
--- /dev/null
+++ b/test/mjsunit/regress/regress-506855825.js
@@ -0,0 +1,11 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --verify-heap --track-array-buffer-views
+
+const ab = new ArrayBuffer(64);
+new Uint8Array(ab);
+
+const evil = { valueOf() { ab.transfer(); return 3; } };
+assertThrows(() => new DataView(ab, 0, evil), TypeError);
Loading diff…
Original Bug Report
reported by hu...@gmail.com
Incorrect implementation of the track-array-buffer-views feature leads to a crash
DETAILS
The execution flow of new DataView(ab, 0, evil_obj); is as follows:
array_buffer->was_detached()checks whether the buffer has been detached — the PoC passes this check.- Subsequently,
Object::ToIndex(isolate, byte_length, ...)triggersevil_obj.valueOf(), causingabto be detached. - A
JSDataViewobject is created. array_buffer->AttachView(*data_view)is called — note: this step registersdata_viewinto an already-detachedarray_buffer.array_buffer->was_detached()is checked again: an exception is thrown here, but it is too late, because theviewsfield ofarray_bufferhas already been written.
BUILTIN(DataViewConstructor) {
...
// 4. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if (array_buffer->was_detached()) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate,
NewTypeError(
MessageTemplate::kTypedArrayDetachedErrorOperation,
isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
}
...
// 7. Let bufferIsResizable be IsResizableArrayBuffer(buffer).
// 8. Let byteLengthChecked be empty.
// 9. If bufferIsResizable is true and byteLength is undefined, then
// a. Let viewByteLength be auto.
// 10. Else if byteLength is undefined, then
// a. Let viewByteLength be bufferByteLength - offset.
size_t view_byte_length;
bool length_tracking = false;
if (IsUndefined(*byte_length, isolate)) {
view_byte_length = buffer_byte_length - view_byte_offset;
length_tracking = array_buffer->is_resizable_by_js();
} else {
// 11. Else,
// a. Set byteLengthChecked be ? ToIndex(byteLength).
// b. Let viewByteLength be byteLengthChecked.
// c. If offset + viewByteLength > bufferByteLength, throw a
// RangeError exception.
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, byte_length,
Object::ToIndex(isolate, byte_length, // <=== Triggers evil_obj.valueOf()
MessageTemplate::kInvalidDataViewLength));
...
view_byte_length = Object::NumberValue(*byte_length);
}
bool is_backed_by_rab =
array_buffer->is_resizable_by_js() && !array_buffer->is_shared();
// 12. Let O be ? OrdinaryCreateFromConstructor(NewTarget,
// "%DataViewPrototype%", «[[DataView]], [[ViewedArrayBuffer]],
// [[ByteLength]], [[ByteOffset]]»).
DirectHandle<JSObject> result;
if (is_backed_by_rab || length_tracking) {
...
} else {
// Create a JSDataView.
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, result,
JSObject::New(target, new_target, {},
NewJSObjectType::kMaybeEmbedderFieldsAndApiWrapper));
}
auto data_view = Cast<JSDataViewOrRabGsabDataView>(result);
...
// Attach
array_buffer->AttachView(*data_view); // <== Root Cause
// 13. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if (array_buffer->was_detached()) { // <=== Throw exception
THROW_NEW_ERROR_RETURN_FAILURE(
isolate,
NewTypeError(
MessageTemplate::kTypedArrayDetachedErrorOperation,
isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
}
...
}
commit a08ad9c9c382b93980a7e76e65594a734cecadda introduced this vuln.
Fix suggestion: Move array_buffer->AttachView(*data_view); to after the 13. If IsDetachedBuffer(buffer) check.
REPRODUCE
poc.js:
const ab = new ArrayBuffer(64);
let typedArray = new Uint8Array(ab);
const evil_obj = {
valueOf() {
ab.transfer();
return 3;
},
};
let view = new DataView(ab, 0, evil_obj);
V8 must be built with a debug configuration. Execute V8 as follows:
../x64.debug/d8 \
--verify-heap \
--track-array-buffer-views \
./poc.js
This will result in the following crash:
#
# Fatal error in ../../src/diagnostics/objects-debug.cc, line 2903
# Check failed: views.GetHeapObjectAssumeWeak() == view (0x2c7e0104b635 <DataView map = 0x2c7e01029c21> vs. 0x2c7e0104b561 <Uint8Array map = 0x2c7e01039d11>).
#
#
CREDIT INFORMATION
Reporter credit: [303f06e3]
References
On This Page