CVE-2026-10904
Overview
Background
- `DataView` constructor
- the builtin in
src/builtins/builtins-dataview.ccthat creates aJSDataViewover anArrayBuffer, running spec steps that read thebyteLengthargument and validate offsets. - `AttachView`
- the method that registers a newly created view in the backing
ArrayBuffer’sviewstracking list. - Detach / `transfer()`
ArrayBuffer.prototype.transfer()(and detachment generally) invalidates a buffer;array_buffer->was_detached()reports this state.- View-tracking consistency
- a
--verify-heap/--track-array-buffer-viewsinvariant that the buffer’sviewslist agrees with each view’s detached state, checked byCheckArrayBufferViewTrackingConsistency.
Root Cause Analysis
The DataView constructor called array_buffer->AttachView(*data_view) before the second IsDetachedBuffer (was_detached()) check. User code running during argument coercion — the byteLength argument’s valueOf, or a new.target.prototype getter — can call ab.transfer() to detach the buffer between the two checks. In that window the pre-patch code attached the view to the now-detached buffer and then threw a TypeError at the detach check, so the constructor returned failure but left an orphan view registered in the buffer’s views field, violating the tracking invariant verified by --verify-heap.
The fix removes the early AttachView and re-inserts it immediately after the was_detached() check, so a buffer detached during coercion is never given a view that is never returned to script.
The patch keeps AttachView before the remaining RAB byte-length and offset/length range checks because those steps run no user code and cannot detach the buffer, while deferring past them would create the opposite inconsistency (a view with views == kNoView while WasDetached() is false).
AttachView before the post-coercion detach check, letting a valueOf/getter-triggered transfer() leave an orphan view tracked on a detached buffer; the fix moves AttachView to just after the detach check but still before the (user-code-free) range checks.Attack Path
- Create a buffer and a view
Allocate
const ab = new ArrayBuffer(64)and attach an initial view (new Uint8Array(ab)), matching the shipped regression test. - Supply a detaching coercion object
Pass a
byteLengthargument whosevalueOf()callsab.transfer()(or anew.target.prototypegetter that detaches) so user code runs during constructor argument coercion. - Invoke the DataView constructor
Call
new DataView(ab, 0, evil); pre-patch,AttachViewregisters the new view before the detach check runs. - Leave an orphan tracked view
The subsequent
was_detached()check throwsTypeError, but the view remains in the detached buffer’sviewslist, breakingCheckArrayBufferViewTrackingConsistency(observable under--verify-heap).
Impact Assessment
DataView constructor into leaving a tracking-list inconsistency between an ArrayBuffer and a view that was never returned to script, an inappropriate implementation / logic error in V8. The precondition is detaching the buffer via a valueOf/getter side effect during DataView construction; the advisory classifies it as a LogicError (“Inappropriate implementation in V8”) and does not report exploitation in the wild. The patch demonstrates only the broken tracking invariant (surfaced by --verify-heap); any escalation to memory corruption is (inferred) and not shown by the diff.Files Changed
src/builtins/builtins-dataview.cctest/mjsunit/regress/regress-506855825.js
Audit Directions
- Register-before-validate orderingAudit object constructors that register/link a new object into shared bookkeeping before the last validation step that can throw, especially where argument coercion can run user code in between.
- Coercion side effects (`valueOf`/getters)Review every builtin that reads user-supplied lengths/offsets or
new.target.prototypefor detach/resize side effects occurring between related state checks. - Array-buffer view trackingCross-check all
AttachView/detach/resize call sites againstCheckArrayBufferViewTrackingConsistencyto ensure theviewslist and each view’sWasDetached()state stay consistent on every throw path.
Patch
From 33395e4fe24b9dc5b435db658d992f68c758af43 Mon Sep 17 00:00:00 2001 From: Jakob Linke <[email protected]> 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 <[email protected]> Reviewed-by: Olivier Flückiger <[email protected]> Commit-Queue: Olivier Flückiger <[email protected]> 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);
Regression Test / PoC
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);
Original Bug Report
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]