Bug 1633598 - Add JSAPI to create an ArrayBuffer with contents and length copied from an existing one. r=sfink
☠☠ backed out by 16e24bb092f6 ☠ ☠
authorJeff Walden <jwalden@mit.edu>
Thu, 07 May 2020 20:52:47 +0000
changeset 528686 78ae14106ac70e4815709a6a7a563914fb06760c
parent 528685 414d909e053acd4f04ed30d20f7f6b0c6773d68d
child 528687 a2abc7629ec4385b6d770ab6c993c6770a8df6fe
push id37393
push userrmaries@mozilla.com
push dateFri, 08 May 2020 03:38:07 +0000
treeherdermozilla-central@ead8f0367372 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfink
bugs1633598
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1633598 - Add JSAPI to create an ArrayBuffer with contents and length copied from an existing one. r=sfink Differential Revision: https://phabricator.services.mozilla.com/D72909
js/public/ArrayBuffer.h
js/src/vm/ArrayBufferObject.cpp
js/src/vm/ArrayBufferObject.h
--- a/js/public/ArrayBuffer.h
+++ b/js/public/ArrayBuffer.h
@@ -30,21 +30,48 @@ extern JS_PUBLIC_API JSObject* NewArrayB
 
 /**
  * Create a new ArrayBuffer with the given |contents|, which may be null only
  * if |nbytes == 0|.  |contents| must be allocated compatible with deallocation
  * by |JS_free|.
  *
  * If and only if an ArrayBuffer is successfully created and returned,
  * ownership of |contents| is transferred to the new ArrayBuffer.
+ *
+ * Care must be taken that |nbytes| bytes of |content| remain valid for the
+ * duration of this call.  In particular, passing the length/pointer of existing
+ * typed array or ArrayBuffer data is generally unsafe: if a GC occurs during a
+ * call to this function, it could move those contents to a different location
+ * and invalidate the provided pointer.
  */
 extern JS_PUBLIC_API JSObject* NewArrayBufferWithContents(JSContext* cx,
                                                           size_t nbytes,
                                                           void* contents);
 
+/**
+ * Create a new ArrayBuffer, whose bytes are set to the values of the bytes in
+ * the provided ArrayBuffer.
+ *
+ * |maybeArrayBuffer| is asserted to be non-null.  An error is thrown if
+ * |maybeArrayBuffer| would fail the |IsArrayBufferObject| test given further
+ * below or if |maybeArrayBuffer| is detached.
+ *
+ * |maybeArrayBuffer| may store its contents in any fashion (i.e. it doesn't
+ * matter whether |maybeArrayBuffer| was allocated using |JS::NewArrayBuffer|,
+ * |JS::NewExternalArrayBuffer|, or any other ArrayBuffer-allocating function).
+ *
+ * The newly-created ArrayBuffer is effectively creatd as if by
+ * |JS::NewArrayBufferWithContents| passing in |maybeArrayBuffer|'s internal
+ * data pointer and length, in a manner safe against |maybeArrayBuffer|'s data
+ * being moved around by the GC.  In particular, the new ArrayBuffer will not
+ * behave like one created for WASM or asm.js, so it *can* be detached.
+ */
+extern JS_PUBLIC_API JSObject* CopyArrayBuffer(
+    JSContext* cx, JS::Handle<JSObject*> maybeArrayBuffer);
+
 using BufferContentsFreeFunc = void (*)(void* contents, void* userData);
 
 /**
  * Create a new ArrayBuffer with the given contents. The contents must not be
  * modified by any other code, internal or external.
  *
  * When the ArrayBuffer is ready to be disposed of, `freeFunc(contents,
  * freeUserData)` will be called to release the ArrayBuffer's reference on the
--- a/js/src/vm/ArrayBufferObject.cpp
+++ b/js/src/vm/ArrayBufferObject.cpp
@@ -13,21 +13,23 @@
 #include "mozilla/CheckedInt.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/PodOperations.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/TaggedAnonymousMemory.h"
 
-#include <algorithm>
+#include <algorithm>  // std::max, std::min
+#include <memory>     // std::uninitialized_copy_n
 #include <string.h>
 #ifndef XP_WIN
 #  include <sys/mman.h>
 #endif
+#include <tuple>  // std::tuple
 #ifdef MOZ_VALGRIND
 #  include <valgrind/memcheck.h>
 #endif
 
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "jsnum.h"
 #include "jstypes.h"
@@ -408,16 +410,34 @@ bool ArrayBufferObject::class_constructo
     return false;
   }
   args.rval().setObject(*bufobj);
   return true;
 }
 
 using ArrayBufferContents = UniquePtr<uint8_t[], JS::FreePolicy>;
 
+static ArrayBufferContents AllocateUninitializedArrayBufferContents(
+    JSContext* cx, uint32_t nbytes) {
+  // First attempt a normal allocation.
+  uint8_t* p =
+      cx->maybe_pod_arena_malloc<uint8_t>(js::ArrayBufferContentsArena, nbytes);
+  if (MOZ_UNLIKELY(!p)) {
+    // Otherwise attempt a large allocation, calling the
+    // large-allocation-failure callback if necessary.
+    p = static_cast<uint8_t*>(cx->runtime()->onOutOfMemoryCanGC(
+        js::AllocFunction::Malloc, js::ArrayBufferContentsArena, nbytes));
+    if (!p) {
+      ReportOutOfMemory(cx);
+    }
+  }
+
+  return ArrayBufferContents(p);
+}
+
 static ArrayBufferContents AllocateArrayBufferContents(JSContext* cx,
                                                        uint32_t nbytes) {
   // First attempt a normal allocation.
   uint8_t* p =
       cx->maybe_pod_arena_calloc<uint8_t>(js::ArrayBufferContentsArena, nbytes);
   if (MOZ_UNLIKELY(!p)) {
     // Otherwise attempt a large allocation, calling the
     // large-allocation-failure callback if necessary.
@@ -429,17 +449,17 @@ static ArrayBufferContents AllocateArray
   }
 
   return ArrayBufferContents(p);
 }
 
 static ArrayBufferContents NewCopiedBufferContents(
     JSContext* cx, Handle<ArrayBufferObject*> buffer) {
   ArrayBufferContents dataCopy =
-      AllocateArrayBufferContents(cx, buffer->byteLength());
+      AllocateUninitializedArrayBufferContents(cx, buffer->byteLength());
   if (dataCopy) {
     if (auto count = buffer->byteLength()) {
       memcpy(dataCopy.get(), buffer->dataPointer(), count);
     }
   }
   return dataCopy;
 }
 
@@ -1163,63 +1183,104 @@ ArrayBufferObject* ArrayBufferObject::cr
 
   if (contents.kind() == MAPPED || contents.kind() == MALLOCED) {
     AddCellMemory(buffer, nAllocated, MemoryUse::ArrayBufferContents);
   }
 
   return buffer;
 }
 
-ArrayBufferObject* ArrayBufferObject::createZeroed(
-    JSContext* cx, uint32_t nbytes, HandleObject proto /* = nullptr */) {
-  // 24.1.1.1, step 3 (Inlined 6.2.6.1 CreateByteDataBlock, step 2).
-  if (!CheckArrayBufferTooLarge(cx, nbytes)) {
-    return nullptr;
-  }
+template <ArrayBufferObject::FillContents FillType>
+/* static */ std::tuple<ArrayBufferObject*, uint8_t*>
+ArrayBufferObject::createBufferAndData(
+    JSContext* cx, uint32_t nbytes, AutoSetNewObjectMetadata&,
+    JS::Handle<JSObject*> proto /* = nullptr */) {
+  MOZ_ASSERT(nbytes <= ArrayBufferObject::MaxBufferByteLength,
+             "caller must validate the byte count it passes");
 
   // Try fitting the data inline with the object by repurposing fixed-slot
   // storage.  Add extra fixed slots if necessary to accomplish this, but don't
   // exceed the maximum number of fixed slots!
   size_t nslots = JSCLASS_RESERVED_SLOTS(&class_);
   ArrayBufferContents data;
   if (nbytes <= MaxInlineBytes) {
     int newSlots = HowMany(nbytes, sizeof(Value));
     MOZ_ASSERT(int(nbytes) <= newSlots * int(sizeof(Value)));
 
     nslots += newSlots;
   } else {
-    data = AllocateArrayBufferContents(cx, nbytes);
+    data = (FillType == FillContents::Uninitialized
+                ? AllocateUninitializedArrayBufferContents
+                : AllocateArrayBufferContents)(cx, nbytes);
     if (!data) {
-      return nullptr;
+      return {nullptr, nullptr};
     }
   }
 
   MOZ_ASSERT(!(class_.flags & JSCLASS_HAS_PRIVATE));
   gc::AllocKind allocKind = GetArrayBufferGCObjectKind(nslots);
 
-  AutoSetNewObjectMetadata metadata(cx);
-  Rooted<ArrayBufferObject*> buffer(
-      cx, NewObjectWithClassProto<ArrayBufferObject>(cx, proto, allocKind,
-                                                     GenericObject));
+  ArrayBufferObject* buffer = NewObjectWithClassProto<ArrayBufferObject>(
+      cx, nullptr, allocKind, GenericObject);
   if (!buffer) {
-    return nullptr;
+    return {nullptr, nullptr};
   }
 
   MOZ_ASSERT(!gc::IsInsideNursery(buffer),
              "ArrayBufferObject has a finalizer that must be called to not "
              "leak in some cases, so it can't be nursery-allocated");
 
+  uint8_t* toFill;
   if (data) {
-    buffer->initialize(nbytes, BufferContents::createMalloced(data.release()));
+    toFill = data.release();
+    buffer->initialize(nbytes, BufferContents::createMalloced(toFill));
     AddCellMemory(buffer, nbytes, MemoryUse::ArrayBufferContents);
   } else {
-    void* inlineData = buffer->initializeToInlineData(nbytes);
-    memset(inlineData, 0, nbytes);
+    toFill = static_cast<uint8_t*>(buffer->initializeToInlineData(nbytes));
+    if constexpr (FillType == FillContents::Zero) {
+      memset(toFill, 0, nbytes);
+    }
+  }
+
+  return {buffer, toFill};
+}
+
+/* static */ ArrayBufferObject* ArrayBufferObject::copy(
+    JSContext* cx, JS::Handle<ArrayBufferObject*> unwrappedArrayBuffer) {
+  if (unwrappedArrayBuffer->isDetached()) {
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_TYPED_ARRAY_DETACHED);
+    return nullptr;
   }
 
+  uint32_t nbytes = unwrappedArrayBuffer->byteLength();
+
+  AutoSetNewObjectMetadata metadata(cx);
+  auto [buffer, toFill] = createBufferAndData<FillContents::Uninitialized>(
+      cx, nbytes, metadata, nullptr);
+  if (!buffer) {
+    return nullptr;
+  }
+
+  std::uninitialized_copy_n(unwrappedArrayBuffer->dataPointer(), nbytes,
+                            toFill);
+  return buffer;
+}
+
+ArrayBufferObject* ArrayBufferObject::createZeroed(
+    JSContext* cx, uint32_t nbytes, HandleObject proto /* = nullptr */) {
+  // 24.1.1.1, step 3 (Inlined 6.2.6.1 CreateByteDataBlock, step 2).
+  if (!CheckArrayBufferTooLarge(cx, nbytes)) {
+    return nullptr;
+  }
+
+  AutoSetNewObjectMetadata metadata(cx);
+  auto [buffer, toFill] =
+      createBufferAndData<FillContents::Zero>(cx, nbytes, metadata, proto);
+  Unused << toFill;
   return buffer;
 }
 
 ArrayBufferObject* ArrayBufferObject::createForTypedObject(JSContext* cx,
                                                            uint32_t nbytes) {
   ArrayBufferObject* buffer = createZeroed(cx, nbytes);
   if (buffer) {
     buffer->setHasTypedObjectViews();
@@ -1669,16 +1730,49 @@ JS_PUBLIC_API JSObject* JS::NewArrayBuff
   }
 
   using BufferContents = ArrayBufferObject::BufferContents;
 
   BufferContents contents = BufferContents::createMalloced(data);
   return ArrayBufferObject::createForContents(cx, nbytes, contents);
 }
 
+static ArrayBufferObject* UnwrapArrayBuffer(
+    JSContext* cx, JS::Handle<JSObject*> maybeArrayBuffer) {
+  JSObject* obj = CheckedUnwrapStatic(maybeArrayBuffer);
+  if (!obj) {
+    ReportAccessDenied(cx);
+    return nullptr;
+  }
+
+  if (!obj->is<ArrayBufferObject>()) {
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_TYPED_ARRAY_BAD_ARGS);
+    return nullptr;
+  }
+
+  return &obj->as<ArrayBufferObject>();
+}
+
+JS_PUBLIC_API JSObject* JS::CopyArrayBuffer(JSContext* cx,
+                                            Handle<JSObject*> arrayBuffer) {
+  AssertHeapIsIdle();
+  CHECK_THREAD(cx);
+
+  MOZ_ASSERT(arrayBuffer != nullptr);
+
+  Rooted<ArrayBufferObject*> unwrappedSource(
+      cx, UnwrapArrayBuffer(cx, arrayBuffer));
+  if (!unwrappedSource) {
+    return nullptr;
+  }
+
+  return ArrayBufferObject::copy(cx, unwrappedSource);
+}
+
 JS_PUBLIC_API JSObject* JS::NewExternalArrayBuffer(
     JSContext* cx, size_t nbytes, void* data,
     JS::BufferContentsFreeFunc freeFunc, void* freeUserData) {
   AssertHeapIsIdle();
   CHECK_THREAD(cx);
 
   MOZ_ASSERT(data);
   MOZ_ASSERT(nbytes > 0);
--- a/js/src/vm/ArrayBufferObject.h
+++ b/js/src/vm/ArrayBufferObject.h
@@ -4,16 +4,18 @@
  * 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/. */
 
 #ifndef vm_ArrayBufferObject_h
 #define vm_ArrayBufferObject_h
 
 #include "mozilla/Maybe.h"
 
+#include <tuple>  // std::tuple
+
 #include "builtin/TypedObjectConstants.h"
 #include "gc/Memory.h"
 #include "gc/ZoneAllocator.h"
 #include "js/ArrayBuffer.h"
 #include "js/GCHashTable.h"
 #include "vm/JSObject.h"
 #include "vm/Runtime.h"
 #include "vm/SharedMem.h"
@@ -227,16 +229,23 @@ class ArrayBufferObject : public ArrayBu
     // MALLOCED buffer which *can* be prepared.)
     FOR_ASMJS = 0b10'0000,
   };
 
   static_assert(JS_ARRAYBUFFER_DETACHED_FLAG == DETACHED,
                 "self-hosted code with burned-in constants must use the "
                 "correct DETACHED bit value");
 
+  enum class FillContents { Zero, Uninitialized };
+
+  template <FillContents FillType>
+  static std::tuple<ArrayBufferObject*, uint8_t*> createBufferAndData(
+      JSContext* cx, uint32_t nbytes, AutoSetNewObjectMetadata&,
+      JS::Handle<JSObject*> proto = nullptr);
+
  public:
   class BufferContents {
     uint8_t* data_;
     BufferKind kind_;
     JS::BufferContentsFreeFunc free_;
     void* freeUserData_;
 
     friend class ArrayBufferObject;
@@ -314,16 +323,19 @@ class ArrayBufferObject : public ArrayBu
 
   static bool fun_species(JSContext* cx, unsigned argc, Value* vp);
 
   static bool class_constructor(JSContext* cx, unsigned argc, Value* vp);
 
   static ArrayBufferObject* createForContents(JSContext* cx, uint32_t nbytes,
                                               BufferContents contents);
 
+  static ArrayBufferObject* copy(
+      JSContext* cx, JS::Handle<ArrayBufferObject*> unwrappedArrayBuffer);
+
   static ArrayBufferObject* createZeroed(JSContext* cx, uint32_t nbytes,
                                          HandleObject proto = nullptr);
 
   static ArrayBufferObject* createForTypedObject(JSContext* cx,
                                                  uint32_t nbytes);
 
   // Create an ArrayBufferObject that is safely finalizable and can later be
   // initialize()d to become a real, content-visible ArrayBufferObject.