| // Copyright 2018 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. |
| |
| #include "src/objects/js-array-buffer.h" |
| |
| #include "src/execution/protectors-inl.h" |
| #include "src/logging/counters.h" |
| #include "src/objects/js-array-buffer-inl.h" |
| #include "src/objects/property-descriptor.h" |
| #include "src/sandbox/check.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| |
| // https://tc39.es/ecma262/#sec-canonicalnumericindexstring |
| // Returns true if the lookup_key represents a valid index string. |
| bool CanonicalNumericIndexString(Isolate* isolate, |
| const PropertyKey& lookup_key, |
| bool* is_minus_zero) { |
| // 1. Assert: Type(argument) is String. |
| DCHECK(lookup_key.is_element() || IsString(*lookup_key.name())); |
| *is_minus_zero = false; |
| if (lookup_key.is_element()) return true; |
| |
| DirectHandle<String> key = Cast<String>(lookup_key.name()); |
| |
| // 3. Let n be ! ToNumber(argument). |
| DirectHandle<Object> result = String::ToNumber(isolate, key); |
| if (IsMinusZero(*result)) { |
| // 2. If argument is "-0", return -0𝔽. |
| // We are not performing SaveValue check for -0 because it'll be rejected |
| // anyway. |
| *is_minus_zero = true; |
| } else { |
| // 4. If SameValue(! ToString(n), argument) is false, return undefined. |
| DirectHandle<String> str = |
| Object::ToString(isolate, result).ToHandleChecked(); |
| // Avoid treating strings like "2E1" and "20" as the same key. |
| if (!Object::SameValue(*str, *key)) return false; |
| } |
| return true; |
| } |
| } // anonymous namespace |
| |
| void JSArrayBuffer::Setup(SharedFlag shared, ResizableFlag resizable, |
| std::shared_ptr<BackingStore> backing_store, |
| Isolate* isolate, Tagged<MaybeObject> views) { |
| auto finish_setup = [shared, isolate]() { |
| // Count usage may lead to a blink allocation, through the callback, which |
| // may trigger a GC. It is important to delay this, until the array buffer |
| // is properly initialized. |
| if (shared) { |
| isolate->CountUsage( |
| v8::Isolate::UseCounterFeature::kSharedArrayBufferConstructed); |
| } |
| }; |
| clear_padding(); |
| init_extension(); |
| set_views_or_detach_key(views); |
| set_bit_field(0); |
| set_is_shared(shared); |
| set_is_resizable_by_js(resizable); |
| set_is_detachable(!shared); |
| cpp_heap_wrappable_.SetupLazilyInitialized(); |
| for (int i = 0; i < v8::ArrayBuffer::kEmbedderFieldCount; i++) { |
| SetEmbedderField(i, Smi::zero()); |
| } |
| if (!backing_store) { |
| set_backing_store(isolate, EmptyBackingStoreBuffer()); |
| set_byte_length(0); |
| set_max_byte_length(0); |
| finish_setup(); |
| return; |
| } |
| // Rest of the code here deals with attaching the BackingStore. |
| DCHECK_EQ(is_shared(), backing_store->is_shared()); |
| DCHECK((is_resizable_by_js() == backing_store->is_resizable_by_js()) || |
| (backing_store->is_wasm_memory() && is_shared())); |
| DCHECK_IMPLIES( |
| !backing_store->is_wasm_memory() && !backing_store->is_resizable_by_js(), |
| backing_store->byte_length() == backing_store->max_byte_length()); |
| |
| void* backing_store_buffer = backing_store->buffer_start(); |
| // Wasm memory always needs a backing store; this is guaranteed by reserving |
| // at least one page for the BackingStore (so {IsEmpty()} is always false). |
| DCHECK_IMPLIES(backing_store->is_wasm_memory(), !backing_store->IsEmpty()); |
| // Non-empty backing stores must start at a non-null pointer. |
| DCHECK_IMPLIES(backing_store_buffer == EmptyBackingStoreBuffer(), |
| backing_store->IsEmpty()); |
| // Empty backing stores can be backed by an empty buffer pointer or by an |
| // externally provided pointer: Either is acceptable. However, the pointer |
| // must always point into the sandbox, so nullptr is not acceptable if the |
| // sandbox is enabled. |
| DCHECK_IMPLIES(V8_ENABLE_SANDBOX_BOOL, backing_store_buffer != nullptr); |
| set_backing_store(isolate, backing_store_buffer); |
| |
| // GSABs need to read their byte_length from the BackingStore. Maintain the |
| // invariant that their byte_length field is always 0. |
| auto byte_len = |
| (is_shared() && is_resizable_by_js()) ? 0 : backing_store->byte_length(); |
| CHECK_LE(backing_store->byte_length(), kMaxByteLength); |
| set_byte_length(byte_len); |
| |
| // For Wasm memories, it is possible for the backing store maximum to be |
| // different from the JSArrayBuffer maximum. The maximum pages allowed on a |
| // Wasm memory are tracked on the Wasm memory object, and not the |
| // JSArrayBuffer associated with it. |
| auto max_byte_len = is_resizable_by_js() ? backing_store->max_byte_length() |
| : backing_store->byte_length(); |
| set_max_byte_length(max_byte_len); |
| |
| if (backing_store->is_wasm_memory()) { |
| set_is_detachable(false); |
| } |
| |
| CreateExtension(isolate, std::move(backing_store)); |
| finish_setup(); |
| } |
| |
| Maybe<bool> JSArrayBuffer::Detach(DirectHandle<JSArrayBuffer> buffer, |
| bool force_for_wasm_memory, |
| DirectHandle<Object> maybe_key) { |
| Isolate* const isolate = Isolate::Current(); |
| |
| bool key_mismatch = false; |
| |
| auto key = buffer->DetachKey(isolate); |
| if (!IsUndefined(key)) { |
| key_mismatch = |
| maybe_key.is_null() || !Object::StrictEquals(*maybe_key, key); |
| } else { |
| // Detach key is undefined; allow not passing maybe_key but disallow passing |
| // something else than undefined. |
| key_mismatch = !maybe_key.is_null() && !IsUndefined(*maybe_key); |
| } |
| if (key_mismatch) { |
| THROW_NEW_ERROR( |
| isolate, |
| NewTypeError(MessageTemplate::kArrayBufferDetachKeyDoesntMatch)); |
| } |
| |
| if (buffer->is_immutable()) { |
| THROW_NEW_ERROR( |
| isolate, |
| NewTypeError(MessageTemplate::kTypedArrayImmutableBufferErrorOperation, |
| isolate->factory()->NewStringFromAsciiChecked( |
| "DetachArrayBuffer"))); |
| } |
| |
| if (buffer->was_detached()) return Just(true); |
| |
| if (force_for_wasm_memory) { |
| // Skip the is_detachable() check. |
| } else if (!buffer->is_detachable()) { |
| // Not detachable, do nothing. |
| return Just(true); |
| } |
| |
| DetachInternal(buffer, force_for_wasm_memory, isolate); |
| return Just(true); |
| } |
| |
| // static |
| void JSArrayBuffer::SetDetachKey(DirectHandle<JSArrayBuffer> array_buffer, |
| DirectHandle<Object> key, Isolate* isolate) { |
| if (IsUndefined(*key) && !array_buffer->has_detach_key()) { |
| return; |
| } |
| DirectHandle<Cell> cell = isolate->factory()->NewCell(); |
| cell->set_value(*key); |
| array_buffer->set_detach_key(*cell); |
| } |
| |
| // static |
| void JSTypedArray::MarkDetached(DirectHandle<JSTypedArray> typed_array, |
| Isolate* isolate) { |
| DCHECK(!typed_array->is_on_heap()); |
| DirectHandle<Map> current_map(typed_array->map(), isolate); |
| MapUpdater update(isolate, current_map); |
| DirectHandle<Map> new_map = update.ChangeInstanceType( |
| MapUpdater::InstanceTypeChange::kTypedArrayDetaching); |
| JSObject::MigrateToMap(isolate, typed_array, new_map); |
| typed_array->WriteBoundedSizeField(offsetof(JSTypedArray, raw_length_), 0); |
| typed_array->WriteBoundedSizeField( |
| offsetof(JSArrayBufferView, raw_byte_length_), 0); |
| // TODO(olivf, 467645277): Set the buffer to a canonical detached ab value. |
| } |
| |
| // static |
| bool JSArrayBuffer::TryDetachViews(DirectHandle<JSArrayBuffer> array_buffer, |
| Isolate* isolate) { |
| if (!v8_flags.track_array_buffer_views) return false; |
| |
| Tagged<MaybeObject> views = array_buffer->views(); |
| if (views == kNoView) return true; |
| if (views == kManyViews) return false; |
| Tagged<HeapObject> view_heap_object; |
| if (!views.GetHeapObjectIfWeak(&view_heap_object)) { |
| return true; |
| } |
| if (IsJSTypedArray(view_heap_object)) { |
| DirectHandle<JSTypedArray> typed_array(Cast<JSTypedArray>(view_heap_object), |
| isolate); |
| JSTypedArray::MarkDetached(typed_array, isolate); |
| array_buffer->set_views(JSArrayBuffer::kNoView); |
| return true; |
| } |
| // No need to track further if we failed to update the view. |
| array_buffer->set_views(kManyViews); |
| return false; |
| } |
| |
| // static |
| void JSArrayBuffer::DetachInternal(DirectHandle<JSArrayBuffer> array_buffer, |
| bool force_for_wasm_memory, |
| Isolate* isolate) { |
| { |
| DisallowGarbageCollection disallow_gc; |
| ArrayBufferExtension* extension = |
| array_buffer->extract_extension(isolate, disallow_gc); |
| if (extension) { |
| isolate->heap()->DetachArrayBufferExtension(extension); |
| // Prevent concurrent detachment vs background copying if an attacker |
| // swapped the extension pointer to a shared buffer's extension. |
| SBXCHECK(!extension->is_shared()); |
| std::shared_ptr<BackingStore> backing_store = |
| extension->RemoveBackingStore(); |
| CHECK_IMPLIES(force_for_wasm_memory, backing_store->is_wasm_memory()); |
| } |
| } |
| |
| array_buffer->set_was_detached(true, kReleaseStore); |
| |
| if (Protectors::IsArrayBufferDetachingIntact(isolate) && |
| !TryDetachViews(array_buffer, isolate)) { |
| Protectors::InvalidateArrayBufferDetaching(isolate); |
| } |
| |
| DCHECK(!array_buffer->is_shared()); |
| array_buffer->set_backing_store(isolate, EmptyBackingStoreBuffer()); |
| array_buffer->set_byte_length(0); |
| } |
| |
| void JSArrayBuffer::MakeImmutable(Isolate* isolate) { |
| if (is_immutable()) return; |
| DCHECK(!was_detached()); |
| set_is_immutable(ImmutableFlag{true}); |
| if (auto backing_store = GetBackingStore()) { |
| backing_store->set_is_immutable(true); |
| } |
| if (Protectors::IsArrayBufferMutableIntact(isolate)) { |
| Protectors::InvalidateArrayBufferMutable(isolate); |
| } |
| } |
| |
| size_t JSArrayBuffer::GsabByteLength(Isolate* isolate, |
| Address raw_array_buffer) { |
| // TODO(v8:11111): Cache the last seen length in JSArrayBuffer and use it |
| // in bounds checks to minimize the need for calling this function. |
| DisallowGarbageCollection no_gc; |
| DisallowJavascriptExecution no_js(isolate); |
| Tagged<JSArrayBuffer> buffer = |
| Cast<JSArrayBuffer>(Tagged<Object>(raw_array_buffer)); |
| CHECK(buffer->is_resizable_by_js()); |
| CHECK(buffer->is_shared()); |
| return buffer->GetBackingStore()->byte_length(std::memory_order_seq_cst); |
| } |
| |
| // static |
| Maybe<bool> JSArrayBuffer::GetResizableBackingStorePageConfiguration( |
| Isolate* isolate, size_t byte_length, size_t max_byte_length, |
| ShouldThrow should_throw, size_t* page_size, size_t* initial_pages, |
| size_t* max_pages) { |
| DCHECK_NOT_NULL(page_size); |
| DCHECK_NOT_NULL(initial_pages); |
| DCHECK_NOT_NULL(max_pages); |
| |
| *page_size = AllocatePageSize(); |
| |
| if (!RoundUpToPageSize(byte_length, *page_size, JSArrayBuffer::kMaxByteLength, |
| initial_pages)) { |
| if (should_throw == kDontThrow) return Nothing<bool>(); |
| THROW_NEW_ERROR(isolate, |
| NewRangeError(MessageTemplate::kInvalidArrayBufferLength)); |
| } |
| |
| if (!RoundUpToPageSize(max_byte_length, *page_size, |
| JSArrayBuffer::kMaxByteLength, max_pages)) { |
| if (should_throw == kDontThrow) return Nothing<bool>(); |
| THROW_NEW_ERROR( |
| isolate, NewRangeError(MessageTemplate::kInvalidArrayBufferMaxLength)); |
| } |
| |
| return Just(true); |
| } |
| |
| // static |
| std::optional<MessageTemplate> |
| JSArrayBuffer::GetResizableBackingStorePageConfigurationImpl( |
| Isolate* isolate, size_t byte_length, size_t max_byte_length, |
| size_t* page_size, size_t* initial_pages, size_t* max_pages) { |
| DCHECK_NOT_NULL(page_size); |
| DCHECK_NOT_NULL(initial_pages); |
| DCHECK_NOT_NULL(max_pages); |
| |
| *page_size = AllocatePageSize(); |
| |
| if (!RoundUpToPageSize(byte_length, *page_size, JSArrayBuffer::kMaxByteLength, |
| initial_pages)) { |
| return MessageTemplate::kInvalidArrayBufferLength; |
| } |
| |
| if (!RoundUpToPageSize(max_byte_length, *page_size, |
| JSArrayBuffer::kMaxByteLength, max_pages)) { |
| return MessageTemplate::kInvalidArrayBufferMaxLength; |
| } |
| return {}; |
| } |
| |
| ArrayBufferExtension* JSArrayBuffer::CreateExtension( |
| Isolate* isolate, std::shared_ptr<BackingStore> backing_store) { |
| // `Heap::InYoungGeneration` during full GC with sticky markbits is generally |
| // inaccurate. However, a full GC will sweep both lists and promote all to |
| // old, so it doesn't matter which list initially holds the extension in this |
| // case. |
| const auto age = HeapLayout::InYoungGeneration(Tagged<JSArrayBuffer>(this)) |
| ? ArrayBufferExtension::Age::kYoung |
| : ArrayBufferExtension::Age::kOld; |
| ArrayBufferExtension* extension = new ArrayBufferExtension( |
| std::move(backing_store), age, is_shared(), is_resizable_by_js()); |
| set_extension(extension); |
| isolate->heap()->AppendArrayBufferExtension(extension); |
| return extension; |
| } |
| |
| Handle<JSArrayBuffer> JSTypedArray::GetBuffer(Isolate* isolate) { |
| DirectHandle<JSTypedArray> self(this, isolate); |
| DCHECK(IsTypedArrayOrRabGsabTypedArrayElementsKind(self->GetElementsKind())); |
| Handle<JSArrayBuffer> array_buffer(Cast<JSArrayBuffer>(self->buffer()), |
| isolate); |
| if (!is_on_heap()) { |
| // Already is off heap, so return the existing buffer. |
| return array_buffer; |
| } |
| DCHECK(!array_buffer->is_resizable_by_js()); |
| |
| // The existing array buffer should be empty. |
| DCHECK(array_buffer->IsEmpty()); |
| |
| // Allocate a new backing store and attach it to the existing array buffer. |
| size_t byte_length = self->byte_length(); |
| auto backing_store = BackingStore::Allocate( |
| isolate, byte_length, SharedFlag{false}, InitializedFlag{false}); |
| |
| if (!backing_store) { |
| isolate->heap()->FatalProcessOutOfMemory("JSTypedArray::GetBuffer"); |
| } |
| |
| // Copy the elements into the backing store of the array buffer. |
| if (byte_length > 0) { |
| memcpy(backing_store->buffer_start(), self->DataPtr(), byte_length); |
| } |
| |
| // Since the buffer was never materialized before it can only have this one |
| // view. |
| DCHECK_IMPLIES(v8_flags.track_array_buffer_views, |
| array_buffer->views().GetHeapObject() == *self); |
| |
| // Attach the backing store to the array buffer. |
| array_buffer->Setup(SharedFlag{false}, ResizableFlag{false}, |
| std::move(backing_store), isolate, array_buffer->views()); |
| |
| // Clear the elements of the typed array. |
| self->set_elements(ReadOnlyRoots(isolate).empty_byte_array()); |
| self->SetOffHeapDataPtr(isolate, array_buffer->backing_store(), 0); |
| DCHECK(!self->is_on_heap()); |
| |
| return array_buffer; |
| } |
| |
| // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects-defineownproperty-p-desc |
| // static |
| Maybe<bool> JSTypedArray::DefineOwnProperty(Isolate* isolate, |
| DirectHandle<JSTypedArray> o, |
| DirectHandle<Object> key, |
| PropertyDescriptor* desc, |
| Maybe<ShouldThrow> should_throw) { |
| DCHECK(IsName(*key) || IsNumber(*key)); |
| // 1. If Type(P) is String, then |
| PropertyKey lookup_key(isolate, key); |
| if (lookup_key.is_element() || IsSmi(*key) || IsString(*key)) { |
| // 1a. Let numericIndex be ! CanonicalNumericIndexString(P) |
| // 1b. If numericIndex is not undefined, then |
| bool is_minus_zero = false; |
| if (IsSmi(*key) || // Smi keys are definitely canonical |
| CanonicalNumericIndexString(isolate, lookup_key, &is_minus_zero)) { |
| // 1b i. If IsValidIntegerIndex(O, numericIndex) is false, return false. |
| |
| // IsValidIntegerIndex: |
| size_t index = lookup_key.index(); |
| bool out_of_bounds = false; |
| size_t length = o->GetLengthOrOutOfBounds(out_of_bounds); |
| if (o->WasDetached() || out_of_bounds || index >= length) { |
| RETURN_FAILURE(isolate, GetShouldThrow(isolate, should_throw), |
| NewTypeError(MessageTemplate::kInvalidTypedArrayIndex)); |
| } |
| if (Cast<JSArrayBuffer>(o->buffer())->is_immutable()) { |
| // 10.4.5.3 [[DefineOwnProperty]] ( P, Desc ) |
| // step 1.b.ii. If IsImmutableBuffer(O.[[ViewedArrayBuffer]]) is true... |
| // We need to validate that the new descriptor is compatible with the |
| // existing immutable property (which is non-configurable, |
| // non-writable). |
| |
| Handle<Object> current_value; |
| LookupIterator it(isolate, o, index, LookupIterator::OWN); |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, current_value, Object::GetProperty(&it), Nothing<bool>()); |
| |
| if (PropertyDescriptor::IsAccessorDescriptor(desc) || |
| (desc->has_configurable() && desc->configurable()) || |
| (desc->has_enumerable() && !desc->enumerable()) || |
| (desc->has_writable() && desc->writable()) || |
| (desc->has_value() && |
| !Object::SameValue(*desc->value(), *current_value))) { |
| RETURN_FAILURE( |
| isolate, GetShouldThrow(isolate, should_throw), |
| NewTypeError(MessageTemplate::kRedefineDisallowed, key)); |
| } |
| return Just(true); |
| } |
| if (!lookup_key.is_element() || is_minus_zero) { |
| RETURN_FAILURE(isolate, GetShouldThrow(isolate, should_throw), |
| NewTypeError(MessageTemplate::kInvalidTypedArrayIndex)); |
| } |
| |
| // 1b ii. If Desc has a [[Configurable]] field and if |
| // Desc.[[Configurable]] is false, return false. |
| // 1b iii. If Desc has an [[Enumerable]] field and if Desc.[[Enumerable]] |
| // is false, return false. |
| // 1b iv. If IsAccessorDescriptor(Desc) is true, return false. |
| // 1b v. If Desc has a [[Writable]] field and if Desc.[[Writable]] is |
| // false, return false. |
| |
| if (PropertyDescriptor::IsAccessorDescriptor(desc)) { |
| RETURN_FAILURE(isolate, GetShouldThrow(isolate, should_throw), |
| NewTypeError(MessageTemplate::kRedefineDisallowed, key)); |
| } |
| |
| if ((desc->has_configurable() && !desc->configurable()) || |
| (desc->has_enumerable() && !desc->enumerable()) || |
| (desc->has_writable() && !desc->writable())) { |
| RETURN_FAILURE(isolate, GetShouldThrow(isolate, should_throw), |
| NewTypeError(MessageTemplate::kRedefineDisallowed, key)); |
| } |
| |
| // 1b vi. If Desc has a [[Value]] field, perform |
| // ? IntegerIndexedElementSet(O, numericIndex, Desc.[[Value]]). |
| if (desc->has_value()) { |
| if (!desc->has_configurable()) desc->set_configurable(true); |
| if (!desc->has_enumerable()) desc->set_enumerable(true); |
| if (!desc->has_writable()) desc->set_writable(true); |
| DirectHandle<Object> value = desc->value(); |
| LookupIterator it(isolate, o, index, LookupIterator::OWN); |
| RETURN_ON_EXCEPTION_VALUE(isolate, |
| JSObject::DefineOwnPropertyIgnoreAttributes( |
| &it, value, desc->ToAttributes()), |
| Nothing<bool>()); |
| } |
| // 1b vii. Return true. |
| return Just(true); |
| } |
| } |
| // 4. Return ! OrdinaryDefineOwnProperty(O, P, Desc). |
| return JSReceiver::OrdinaryDefineOwnProperty(isolate, o, lookup_key, desc, |
| should_throw); |
| } |
| |
| ExternalArrayType JSTypedArray::type() const { |
| return TypeAndElementSizeFor(map()->elements_kind()).first; |
| } |
| |
| size_t JSTypedArray::element_size() const { |
| return TypeAndElementSizeFor(map()->elements_kind()).second; |
| } |
| |
| size_t JSTypedArray::LengthTrackingGsabBackedTypedArrayLength( |
| Isolate* isolate, Address raw_array) { |
| // TODO(v8:11111): Cache the last seen length in JSArrayBuffer and use it |
| // in bounds checks to minimize the need for calling this function. |
| DisallowGarbageCollection no_gc; |
| DisallowJavascriptExecution no_js(isolate); |
| Tagged<JSTypedArray> array = Cast<JSTypedArray>(Tagged<Object>(raw_array)); |
| CHECK(array->is_length_tracking()); |
| Tagged<JSArrayBuffer> buffer = array->buffer(); |
| CHECK(buffer->is_resizable_by_js()); |
| CHECK(buffer->is_shared()); |
| size_t backing_byte_length = |
| buffer->GetBackingStore()->byte_length(std::memory_order_seq_cst); |
| CHECK_GE(backing_byte_length, array->byte_offset()); |
| auto element_byte_size = ElementsKindToByteSize(array->GetElementsKind()); |
| return (backing_byte_length - array->byte_offset()) / element_byte_size; |
| } |
| |
| size_t JSTypedArray::GetVariableByteLengthOrOutOfBounds( |
| bool& out_of_bounds) const { |
| DCHECK(!WasDetached()); |
| size_t own_byte_offset = byte_offset(); |
| if (is_length_tracking()) { |
| size_t own_element_size = element_size(); |
| if (is_backed_by_rab()) { |
| size_t buffer_byte_length = buffer()->byte_length(); |
| if (own_byte_offset > buffer_byte_length) { |
| out_of_bounds = true; |
| return 0; |
| } |
| // Round down to the nearest multiple of element size. |
| return RoundDown(buffer_byte_length - own_byte_offset, own_element_size); |
| } |
| // GSAB-backed TypedArrays can't be out of bounds. |
| size_t buffer_byte_length = |
| buffer()->GetBackingStore()->byte_length(std::memory_order_seq_cst); |
| SBXCHECK(own_byte_offset <= buffer_byte_length); |
| // Round down to the nearest multiple of element size. |
| return RoundDown(buffer_byte_length - own_byte_offset, own_element_size); |
| } |
| DCHECK(is_backed_by_rab()); |
| size_t own_byte_length = byte_length(); |
| size_t buffer_byte_length = buffer()->byte_length(); |
| if (own_byte_length > buffer_byte_length || |
| own_byte_offset > buffer_byte_length - own_byte_length) { |
| out_of_bounds = true; |
| return 0; |
| } |
| return own_byte_length; |
| } |
| |
| size_t JSTypedArray::GetVariableLengthOrOutOfBounds(bool& out_of_bounds) const { |
| return GetVariableByteLengthOrOutOfBounds(out_of_bounds) / element_size(); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |