/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "vm/ArrayBufferViewObject.h" #include "builtin/DataViewObject.h" #include "gc/Nursery.h" #include "js/ErrorReport.h" #include "js/experimental/TypedData.h" // JS_GetArrayBufferView{Data,Buffer,Length,ByteOffset}, JS_GetObjectAsArrayBufferView, JS_IsArrayBufferViewObject #include "js/SharedArrayBuffer.h" #include "vm/Compartment.h" #include "vm/JSContext.h" #include "vm/TypedArrayObject.h" #include "gc/Nursery-inl.h" #include "vm/ArrayBufferObject-inl.h" #include "vm/NativeObject-inl.h" using namespace js; // This method is used to trace TypedArrayObjects and DataViewObjects. It // updates the object's data pointer if it points to inline data in an object // that was moved. /* static */ void ArrayBufferViewObject::trace(JSTracer* trc, JSObject* obj) { ArrayBufferViewObject* view = &obj->as(); // Update view's data pointer if it moved. if (view->hasBuffer()) { JSObject* bufferObj = &view->bufferValue().toObject(); ArrayBufferObject* buffer = nullptr; if (gc::MaybeForwardedObjectIs(bufferObj)) { buffer = &gc::MaybeForwardedObjectAs(bufferObj); } else if (gc::MaybeForwardedObjectIs( bufferObj)) { buffer = &gc::MaybeForwardedObjectAs(bufferObj); } if (buffer) { size_t offset = view->dataPointerOffset(); MOZ_ASSERT_IF(!buffer->dataPointer(), offset == 0); // The data may or may not be inline with the buffer. The buffer can only // move during a compacting GC, in which case its objectMoved hook has // already updated the buffer's data pointer. view->notifyBufferMoved( static_cast(view->dataPointerEither_()) - offset, buffer->dataPointer()); } } } template <> bool JSObject::is() const { return is() || is(); } void ArrayBufferViewObject::notifyBufferDetached() { MOZ_ASSERT(!isSharedMemory()); MOZ_ASSERT(hasBuffer()); MOZ_ASSERT(!bufferUnshared()->isLengthPinned()); setFixedSlot(LENGTH_SLOT, PrivateValue(size_t(0))); setFixedSlot(BYTEOFFSET_SLOT, PrivateValue(size_t(0))); setFixedSlot(DATA_SLOT, UndefinedValue()); } void ArrayBufferViewObject::notifyBufferResized() { MOZ_ASSERT(!isSharedMemory()); MOZ_ASSERT(hasBuffer()); MOZ_ASSERT(!bufferUnshared()->isLengthPinned()); MOZ_ASSERT(bufferUnshared()->isResizable()); computeResizableLengthAndByteOffset(bytesPerElement()); } void ArrayBufferViewObject::notifyBufferMoved(uint8_t* srcBufStart, uint8_t* dstBufStart) { MOZ_ASSERT(!isSharedMemory()); MOZ_ASSERT(hasBuffer()); if (srcBufStart != dstBufStart) { void* data = dstBufStart + dataPointerOffset(); getFixedSlotRef(DATA_SLOT).unbarrieredSet(PrivateValue(data)); } } /* static */ bool ArrayBufferViewObject::ensureNonInline( JSContext* cx, Handle view) { MOZ_ASSERT(!view->isSharedMemory()); // Create an ArrayBuffer for the data if it was in the view. ArrayBufferObjectMaybeShared* buffer = ensureBufferObject(cx, view); if (!buffer) { return false; } Rooted unsharedBuffer(cx, &buffer->as()); return ArrayBufferObject::ensureNonInline(cx, unsharedBuffer); } /* static */ ArrayBufferObjectMaybeShared* ArrayBufferViewObject::ensureBufferObject( JSContext* cx, Handle thisObject) { if (thisObject->is()) { Rooted typedArray(cx, &thisObject->as()); if (!TypedArrayObject::ensureHasBuffer(cx, typedArray)) { return nullptr; } } return thisObject->bufferEither(); } bool ArrayBufferViewObject::init(JSContext* cx, ArrayBufferObjectMaybeShared* buffer, size_t byteOffset, size_t length, uint32_t bytesPerElement) { MOZ_ASSERT_IF(!buffer, byteOffset == 0); MOZ_ASSERT_IF(buffer, !buffer->isDetached()); MOZ_ASSERT(byteOffset <= ArrayBufferObject::ByteLengthLimit); MOZ_ASSERT(length <= ArrayBufferObject::ByteLengthLimit); MOZ_ASSERT(byteOffset + length <= ArrayBufferObject::ByteLengthLimit); MOZ_ASSERT_IF(is(), length <= TypedArrayObject::ByteLengthLimit / bytesPerElement); // The isSharedMemory property is invariant. Self-hosting code that // sets BUFFER_SLOT or the private slot (if it does) must maintain it by // always setting those to reference shared memory. if (buffer && buffer->is()) { setIsSharedMemory(); } initFixedSlot(BYTEOFFSET_SLOT, PrivateValue(byteOffset)); initFixedSlot(LENGTH_SLOT, PrivateValue(length)); if (buffer) { initFixedSlot(BUFFER_SLOT, ObjectValue(*buffer)); } else { MOZ_ASSERT(!isSharedMemory()); initFixedSlot(BUFFER_SLOT, JS::FalseValue()); } if (buffer) { SharedMem ptr = buffer->dataPointerEither(); initDataPointer(ptr + byteOffset); // Only ArrayBuffers used for inline typed objects can have // nursery-allocated data and we shouldn't see such buffers here. MOZ_ASSERT_IF(buffer->byteLength() > 0, !cx->nursery().isInside(ptr)); } else { MOZ_ASSERT(is()); MOZ_ASSERT(length * bytesPerElement <= FixedLengthTypedArrayObject::INLINE_BUFFER_LIMIT); void* data = fixedData(FixedLengthTypedArrayObject::FIXED_DATA_START); initReservedSlot(DATA_SLOT, PrivateValue(data)); memset(data, 0, length * bytesPerElement); #ifdef DEBUG if (length == 0) { uint8_t* elements = static_cast(data); elements[0] = ZeroLengthArrayData; } #endif } #ifdef DEBUG if (buffer) { size_t viewByteLength = length * bytesPerElement; size_t viewByteOffset = byteOffset; size_t bufferByteLength = buffer->byteLength(); // Unwraps are safe: both are for the pointer value. MOZ_ASSERT_IF(buffer->is(), buffer->dataPointerEither().unwrap(/*safe*/) <= dataPointerEither().unwrap(/*safe*/)); MOZ_ASSERT(bufferByteLength - viewByteOffset >= viewByteLength); MOZ_ASSERT(viewByteOffset <= bufferByteLength); } #endif // ArrayBufferObjects track their views to support detaching. if (buffer && buffer->is()) { if (!buffer->as().addView(cx, this)) { return false; } } return true; } bool ArrayBufferViewObject::initResizable(JSContext* cx, ArrayBufferObjectMaybeShared* buffer, size_t byteOffset, size_t length, uint32_t bytesPerElement, AutoLength autoLength) { MOZ_ASSERT(buffer->isResizable()); if (!init(cx, buffer, byteOffset, length, bytesPerElement)) { return false; } initFixedSlot(AUTO_LENGTH_SLOT, BooleanValue(static_cast(autoLength))); initFixedSlot(INITIAL_LENGTH_SLOT, PrivateValue(length)); initFixedSlot(INITIAL_BYTE_OFFSET_SLOT, PrivateValue(byteOffset)); // Compute the actual byteLength and byteOffset for non-shared buffers. if (!isSharedMemory()) { computeResizableLengthAndByteOffset(bytesPerElement); } MOZ_ASSERT(!isOutOfBounds(), "can't create out-of-bounds views"); return true; } void ArrayBufferViewObject::computeResizableLengthAndByteOffset( size_t bytesPerElement) { MOZ_ASSERT(!isSharedMemory()); MOZ_ASSERT(hasBuffer()); MOZ_ASSERT(bufferUnshared()->isResizable()); size_t byteOffsetStart = initialByteOffset(); size_t bufferByteLength = bufferUnshared()->byteLength(); // Out-of-bounds if the byteOffset exceeds the buffer length. if (byteOffsetStart > bufferByteLength) { setFixedSlot(LENGTH_SLOT, PrivateValue(size_t(0))); setFixedSlot(BYTEOFFSET_SLOT, PrivateValue(size_t(0))); return; } size_t length; if (isAutoLength()) { length = (bufferByteLength - byteOffsetStart) / bytesPerElement; } else { length = initialLength(); // Out-of-bounds if the byteOffset end index exceeds the buffer length. size_t byteOffsetEnd = byteOffsetStart + length * bytesPerElement; if (byteOffsetEnd > bufferByteLength) { setFixedSlot(LENGTH_SLOT, PrivateValue(size_t(0))); setFixedSlot(BYTEOFFSET_SLOT, PrivateValue(size_t(0))); return; } } setFixedSlot(LENGTH_SLOT, PrivateValue(length)); setFixedSlot(BYTEOFFSET_SLOT, PrivateValue(byteOffsetStart)); } size_t ArrayBufferViewObject::bytesPerElement() const { if (is()) { return as().bytesPerElement(); } MOZ_ASSERT(is()); return 1; } bool ArrayBufferViewObject::hasResizableBuffer() const { if (auto* buffer = bufferEither()) { return buffer->isResizable(); } return false; } size_t ArrayBufferViewObject::dataPointerOffset() const { // Views without a buffer have a zero offset. if (!hasBuffer()) { MOZ_ASSERT(byteOffsetSlotValue() == 0); return 0; } // Views on shared buffers store the offset in |byteOffset|. if (isSharedMemory()) { return byteOffsetSlotValue(); } // Can be called during tracing, so the buffer is possibly forwarded. const auto* bufferObj = gc::MaybeForwarded(&bufferValue().toObject()); // Two distinct classes are used for non-shared buffers. MOZ_ASSERT( gc::MaybeForwardedObjectIs(bufferObj) || gc::MaybeForwardedObjectIs(bufferObj)); // Ensure these two classes can be casted to ArrayBufferObject. static_assert( std::is_base_of_v); static_assert( std::is_base_of_v); // Manual cast necessary because the buffer is possibly forwarded. const auto* buffer = static_cast(bufferObj); // Views on resizable buffers store the offset in |initialByteOffset|. if (buffer->isResizable() && !buffer->isDetached()) { return initialByteOffsetValue(); } // Callers expect that this method returns zero for detached buffers. MOZ_ASSERT_IF(buffer->isDetached(), byteOffsetSlotValue() == 0); // Views on fixed-length buffers store the offset in |byteOffset|. return byteOffsetSlotValue(); } mozilla::Maybe ArrayBufferViewObject::byteOffset() const { // |byteOffset| is set to zero for detached or out-of-bounds views, so a // non-zero value indicates the view is in-bounds. size_t byteOffset = byteOffsetSlotValue(); if (byteOffset > 0) { MOZ_ASSERT(!hasDetachedBuffer()); MOZ_ASSERT_IF(hasResizableBuffer(), !isOutOfBounds()); return mozilla::Some(byteOffset); } if (hasDetachedBufferOrIsOutOfBounds()) { return mozilla::Nothing{}; } return mozilla::Some(0); } mozilla::Maybe ArrayBufferViewObject::length() const { // |length| is set to zero for detached or out-of-bounds views, so a non-zero // value indicates the view is in-bounds. size_t length = lengthSlotValue(); if (MOZ_LIKELY(length > 0)) { MOZ_ASSERT(!hasDetachedBuffer()); MOZ_ASSERT_IF(hasResizableBuffer(), !isOutOfBounds()); MOZ_ASSERT(!isSharedMemory() || !hasResizableBuffer() || !isAutoLength(), "length is zero for auto-length growable shared buffers"); return mozilla::Some(length); } if (hasDetachedBufferOrIsOutOfBounds()) { return mozilla::Nothing{}; } if (isSharedMemory()) { auto* buffer = bufferShared(); MOZ_ASSERT(buffer, "shared memory doesn't use inline data"); // Views backed by a growable SharedArrayBuffer can never get out-of-bounds, // but we have to dynamically compute the length when the auto-length flag // is set. if (buffer->isGrowable() && isAutoLength()) { size_t bufferByteLength = buffer->byteLength(); size_t byteOffset = byteOffsetSlotValue(); MOZ_ASSERT(byteOffset <= bufferByteLength); MOZ_ASSERT(byteOffset == initialByteOffset(), "views on growable shared buffers can't get out-of-bounds"); return mozilla::Some((bufferByteLength - byteOffset) / bytesPerElement()); } } return mozilla::Some(0); } #if defined(DEBUG) || defined(JS_JITSPEW) void ArrayBufferViewObject::dumpOwnFields(js::JSONPrinter& json) const { json.formatProperty("length", "%zu", size_t(getFixedSlot(LENGTH_SLOT).toPrivate())); json.formatProperty("byteOffset", "%zu", size_t(getFixedSlot(BYTEOFFSET_SLOT).toPrivate())); void* data = dataPointerEither_(); if (data) { json.formatProperty("data", "0x%p", data); } else { json.nullProperty("data"); } } void ArrayBufferViewObject::dumpOwnStringContent( js::GenericPrinter& out) const { out.printf("length=%zu, byteOffset=%zu, ", size_t(getFixedSlot(LENGTH_SLOT).toPrivate()), size_t(getFixedSlot(BYTEOFFSET_SLOT).toPrivate())); void* data = dataPointerEither_(); if (data) { out.printf("data=0x%p", data); } else { out.put("data=null"); } } #endif /* JS Public API */ JS_PUBLIC_API bool JS_IsArrayBufferViewObject(JSObject* obj) { return obj->canUnwrapAs(); } JS_PUBLIC_API JSObject* js::UnwrapArrayBufferView(JSObject* obj) { return obj->maybeUnwrapIf(); } JS_PUBLIC_API void* JS_GetArrayBufferViewData(JSObject* obj, bool* isSharedMemory, const JS::AutoRequireNoGC&) { ArrayBufferViewObject* view = obj->maybeUnwrapAs(); if (!view) { return nullptr; } *isSharedMemory = view->isSharedMemory(); return view->dataPointerEither().unwrap( /*safe - caller sees isSharedMemory flag*/); } JS_PUBLIC_API uint8_t* JS_GetArrayBufferViewFixedData(JSObject* obj, uint8_t* buffer, size_t bufSize) { ArrayBufferViewObject* view = obj->maybeUnwrapAs(); if (!view) { return nullptr; } // Disallow shared memory until it is needed. if (view->isSharedMemory()) { return nullptr; } // TypedArrays (but not DataViews) can have inline data, in which case we // need to copy into the given buffer. if (view->is()) { auto* ta = &view->as(); if (ta->hasInlineElements()) { size_t bytes = ta->byteLength(); if (bytes > bufSize) { return nullptr; // Does not fit. } memcpy(buffer, view->dataPointerUnshared(), bytes); return buffer; } } return static_cast(view->dataPointerUnshared()); } JS_PUBLIC_API JSObject* JS_GetArrayBufferViewBuffer(JSContext* cx, HandleObject obj, bool* isSharedMemory) { AssertHeapIsIdle(); CHECK_THREAD(cx); cx->check(obj); Rooted unwrappedView( cx, obj->maybeUnwrapAs()); if (!unwrappedView) { ReportAccessDenied(cx); return nullptr; } ArrayBufferObjectMaybeShared* unwrappedBuffer; { AutoRealm ar(cx, unwrappedView); unwrappedBuffer = ArrayBufferViewObject::ensureBufferObject(cx, unwrappedView); if (!unwrappedBuffer) { return nullptr; } } *isSharedMemory = unwrappedBuffer->is(); RootedObject buffer(cx, unwrappedBuffer); if (!cx->compartment()->wrap(cx, &buffer)) { return nullptr; } return buffer; } JS_PUBLIC_API size_t JS_GetArrayBufferViewByteLength(JSObject* obj) { obj = obj->maybeUnwrapAs(); if (!obj) { return 0; } size_t length = obj->is() ? obj->as().byteLength().valueOr(0) : obj->as().byteLength().valueOr(0); return length; } bool JS::ArrayBufferView::isDetached() const { MOZ_ASSERT(obj); return obj->as().hasDetachedBuffer(); } bool JS::ArrayBufferView::isResizable() const { MOZ_ASSERT(obj); return obj->as().hasResizableBuffer(); } JS_PUBLIC_API size_t JS_GetArrayBufferViewByteOffset(JSObject* obj) { obj = obj->maybeUnwrapAs(); if (!obj) { return 0; } size_t offset = obj->is() ? obj->as().byteOffset().valueOr(0) : obj->as().byteOffset().valueOr(0); return offset; } JS_PUBLIC_API mozilla::Span JS::ArrayBufferView::getData( bool* isSharedMemory, const AutoRequireNoGC&) { MOZ_ASSERT(obj->is()); size_t byteLength = obj->is() ? obj->as().byteLength().valueOr(0) : obj->as().byteLength().valueOr(0); ArrayBufferViewObject& view = obj->as(); *isSharedMemory = view.isSharedMemory(); return {static_cast(view.dataPointerEither().unwrap( /*safe - caller sees isShared flag*/)), byteLength}; } JS_PUBLIC_API JSObject* JS_GetObjectAsArrayBufferView(JSObject* obj, size_t* length, bool* isSharedMemory, uint8_t** data) { obj = obj->maybeUnwrapIf(); if (!obj) { return nullptr; } js::GetArrayBufferViewLengthAndData(obj, length, isSharedMemory, data); return obj; } JS_PUBLIC_API void js::GetArrayBufferViewLengthAndData(JSObject* obj, size_t* length, bool* isSharedMemory, uint8_t** data) { JS::AutoAssertNoGC nogc; auto span = JS::ArrayBufferView::fromObject(obj).getData(isSharedMemory, nogc); *data = span.data(); *length = span.Length(); } JS_PUBLIC_API bool JS::IsArrayBufferViewShared(JSObject* obj) { ArrayBufferViewObject* view = obj->maybeUnwrapAs(); if (!view) { return false; } return view->isSharedMemory(); } JS_PUBLIC_API bool JS::IsLargeArrayBufferView(JSObject* obj) { #ifdef JS_64BIT obj = &obj->unwrapAs(); size_t len = obj->is() ? obj->as().byteLength().valueOr(0) : obj->as().byteLength().valueOr(0); return len > ArrayBufferObject::ByteLengthLimitForSmallBuffer; #else // Large ArrayBuffers are not supported on 32-bit. static_assert(ArrayBufferObject::ByteLengthLimit == ArrayBufferObject::ByteLengthLimitForSmallBuffer); return false; #endif } JS_PUBLIC_API bool JS::IsResizableArrayBufferView(JSObject* obj) { auto* view = &obj->unwrapAs(); if (auto* buffer = view->bufferEither()) { return buffer->isResizable(); } return false; } JS_PUBLIC_API bool JS::PinArrayBufferOrViewLength(JSObject* obj, bool pin) { ArrayBufferObjectMaybeShared* buffer = obj->maybeUnwrapIf(); if (buffer) { return buffer->pinLength(pin); } ArrayBufferViewObject* view = obj->maybeUnwrapAs(); if (view) { return view->pinLength(pin); } return false; } JS_PUBLIC_API bool JS::EnsureNonInlineArrayBufferOrView(JSContext* cx, JSObject* obj) { if (obj->is()) { // Always locked and out of line. return true; } auto* buffer = obj->maybeUnwrapIf(); if (buffer) { Rooted rootedBuffer(cx, buffer); return ArrayBufferObject::ensureNonInline(cx, rootedBuffer); } auto* view = obj->maybeUnwrapIf(); if (view) { if (view->isSharedMemory()) { // Always locked and out of line. return true; } Rooted rootedView(cx, view); return ArrayBufferViewObject::ensureNonInline(cx, rootedView); } JS_ReportErrorASCII(cx, "unhandled type"); return false; }