Bug 1503324 - Implement ReadableStreamCreateReadResult. r=arai
authorJason Orendorff <jorendorff@mozilla.com>
Tue, 20 Nov 2018 00:18:07 +0000
changeset 503505 817c31467dcb1ee977a92cf9e07af04da8dbe343
parent 503504 2b2fd36ea6c1ab124817d6826af6d38d3f1306b7
child 503506 a7c904c9581ffe0a02b548521a1bc052441b7715
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1503324
milestone65.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 1503324 - Implement ReadableStreamCreateReadResult. r=arai This prevents author code from using Object.prototype.then to observe or tamper with a stream that is locked by another consumer. Differential Revision: https://phabricator.services.mozilla.com/D12081
js/public/Stream.h
js/src/builtin/Stream.cpp
js/src/builtin/Stream.h
js/src/vm/Iteration.cpp
js/src/vm/Realm.cpp
js/src/vm/Realm.h
testing/web-platform/meta/fetch/api/response/response-stream-with-broken-then.any.js.ini
--- a/js/public/Stream.h
+++ b/js/public/Stream.h
@@ -446,46 +446,47 @@ ReadableStreamEnqueue(JSContext* cx, Han
  *
  * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
  * for one.
  */
 extern JS_PUBLIC_API bool
 ReadableStreamError(JSContext* cx, HandleObject stream, HandleValue error);
 
 /**
- * Cancels the given ReadableStream reader's associated stream.
+ * C++ equivalent of `reader.cancel(reason)`
+ * (both <https://streams.spec.whatwg.org/#default-reader-cancel> and
+ * <https://streams.spec.whatwg.org/#byob-reader-cancel>).
  *
- * Throws a TypeError and returns false if the given reader isn't active.
- *
- * Asserts that |reader| is a ReadableStreamDefaultReader or
- * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
+ * `reader` must be a stream reader created using `JS::ReadableStreamGetReader`
+ * or an unwrappable wrapper for one. (This function is meant to support using
+ * C++ to read from streams. It's not meant to allow C++ code to operate on
+ * readers created by scripts.)
  */
 extern JS_PUBLIC_API bool
 ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason);
 
 /**
- * Cancels the given ReadableStream reader's associated stream.
+ * C++ equivalent of `reader.releaseLock()`
+ * (both <https://streams.spec.whatwg.org/#default-reader-release-lock> and
+ * <https://streams.spec.whatwg.org/#byob-reader-release-lock>).
  *
- * Throws a TypeError and returns false if the given reader has pending
- * read or readInto (for default or byob readers, respectively) requests.
- *
- * Asserts that |reader| is a ReadableStreamDefaultReader or
- * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
+ * `reader` must be a stream reader created using `JS::ReadableStreamGetReader`
+ * or an unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API bool
 ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
 
 /**
- * Requests a read from the reader's associated ReadableStream and returns the
- * resulting PromiseObject.
+ * C++ equivalent of the `reader.read()` method on default readers
+ * (<https://streams.spec.whatwg.org/#default-reader-read>).
  *
- * Returns a Promise that's resolved with the read result once available or
- * rejected immediately if the stream is errored or the operation failed.
+ * The result is a new Promise object, or null on OOM.
  *
- * Asserts that |reader| is a ReadableStreamDefaultReader object or an
- * unwrappable wrapper for one.
+ * `reader` must be the result of calling `JS::ReadableStreamGetReader` with
+ * `ReadableStreamReaderMode::Default` mode, or an unwrappable wrapper for such
+ * a reader.
  */
 extern JS_PUBLIC_API JSObject*
 ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject reader);
 
 } // namespace JS
 
 #endif // js_Realm_h
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -737,17 +737,19 @@ ReadableStream_cancel(JSContext* cx, uns
     if (!cancelPromise) {
         return false;
     }
     args.rval().setObject(*cancelPromise);
     return true;
 }
 
 static MOZ_MUST_USE ReadableStreamDefaultReader*
-CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> unwrappedStream);
+CreateReadableStreamDefaultReader(JSContext* cx,
+                                  Handle<ReadableStream*> unwrappedStream,
+                                  ForAuthorCodeBool forAuthorCode = ForAuthorCodeBool::No);
 
 /**
  * Streams spec, 3.2.5.3. getReader()
  */
 static bool
 ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
@@ -767,17 +769,17 @@ ReadableStream_getReader(JSContext* cx, 
     HandleValue optionsVal = args.get(0);
     if (!optionsVal.isUndefined()) {
         if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) {
             return false;
         }
     }
 
     if (modeVal.isUndefined()) {
-        reader = CreateReadableStreamDefaultReader(cx, unwrappedStream);
+        reader = CreateReadableStreamDefaultReader(cx, unwrappedStream, ForAuthorCodeBool::Yes);
     } else {
         // Step 3: Set mode to ? ToString(mode) (implicit).
         RootedString mode(cx, ToString<CanGC>(cx, modeVal));
         if (!mode) {
             return false;
         }
 
         // Step 4: If mode is "byob", return ? AcquireReadableStreamBYOBReader(this).
@@ -1287,18 +1289,22 @@ ReadableStreamTee(JSContext* cx,
 
 inline static MOZ_MUST_USE bool
 AppendToListAtSlot(JSContext* cx,
                    HandleNativeObject unwrappedContainer,
                    uint32_t slot,
                    HandleObject obj);
 
 /**
- * Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream )
- * Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream )
+ * Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream, forAuthorCode )
+ * Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream, forAuthorCode )
+ *
+ * Our implementation does not pass around forAuthorCode parameters in the same
+ * places as the standard, but the effect is the same. See the comment on
+ * `ReadableStreamReader::forAuthorCode()`.
  */
 static MOZ_MUST_USE JSObject*
 ReadableStreamAddReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> unwrappedStream)
 {
     // Step 1: Assert: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true.
     // Skipped: handles both kinds of readers.
     Rooted<ReadableStreamReader*> unwrappedReader(cx, UnwrapReaderFromStream(cx, unwrappedStream));
     if (!unwrappedReader) {
@@ -1309,20 +1315,23 @@ ReadableStreamAddReadOrReadIntoRequest(J
     MOZ_ASSERT_IF(unwrappedReader->is<ReadableStreamDefaultReader>(), unwrappedStream->readable());
 
     // Step 3: Let promise be a new promise.
     RootedObject promise(cx, PromiseObject::createSkippingExecutor(cx));
     if (!promise) {
         return nullptr;
     }
 
-    // Step 4: Let read{Into}Request be Record {[[promise]]: promise}.
+    // Step 4: Let read{Into}Request be
+    //         Record {[[promise]]: promise, [[forAuthorCode]]: forAuthorCode}.
     // Step 5: Append read{Into}Request as the last element of
     //         stream.[[reader]].[[read{Into}Requests]].
-    // Since [[promise]] is the Record's only field, we store it directly.
+    // Since we don't need the [[forAuthorCode]] field (see the comment on
+    // `ReadableStreamReader::forAuthorCode()`), we elide the Record and store
+    // only the promise.
     if (!AppendToListAtSlot(cx, unwrappedReader, ReadableStreamReader::Slot_Requests, promise)) {
         return nullptr;
     }
 
     // Step 6: Return promise.
     return promise;
 }
 
@@ -1391,16 +1400,22 @@ ReadableStreamCancel(JSContext* cx, Hand
     RootedAtom funName(cx, cx->names().empty);
     RootedFunction returnUndefined(cx, NewNativeFunction(cx, ReturnUndefined, 0, funName));
     if (!returnUndefined) {
         return nullptr;
     }
     return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, nullptr);
 }
 
+static MOZ_MUST_USE JSObject*
+ReadableStreamCreateReadResult(JSContext* cx,
+                               HandleValue value,
+                               bool done,
+                               ForAuthorCodeBool forAuthorCode);
+
 /**
  * Streams spec, 3.4.4. ReadableStreamClose ( stream )
  */
 MOZ_MUST_USE bool
 ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> unwrappedStream)
 {
     // Step 1: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(unwrappedStream->readable());
@@ -1416,32 +1431,36 @@ ReadableStreamCloseInternal(JSContext* c
     // Step 3: Let reader be stream.[[reader]].
     Rooted<ReadableStreamReader*> unwrappedReader(cx, UnwrapReaderFromStream(cx, unwrappedStream));
     if (!unwrappedReader) {
         return false;
     }
 
     // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
     if (unwrappedReader->is<ReadableStreamDefaultReader>()) {
+        ForAuthorCodeBool forAuthorCode = unwrappedReader->forAuthorCode();
+
         // Step a: Repeat for each readRequest that is an element of
         //         reader.[[readRequests]],
         RootedNativeObject unwrappedReadRequests(cx, unwrappedReader->requests());
         uint32_t len = unwrappedReadRequests->getDenseInitializedLength();
         RootedObject readRequest(cx);
         RootedObject resultObj(cx);
         RootedValue resultVal(cx);
         for (uint32_t i = 0; i < len; i++) {
             // Step i: Resolve readRequest.[[promise]] with
-            //         ! CreateIterResultObject(undefined, true).
+            //         ! ReadableStreamCreateReadResult(undefined, true,
+            //                                          readRequest.[[forAuthorCode]]).
             readRequest = &unwrappedReadRequests->getDenseElement(i).toObject();
             if (!cx->compartment()->wrap(cx, &readRequest)) {
                 return false;
             }
 
-            resultObj = CreateIterResultObject(cx, UndefinedHandleValue, true);
+            resultObj = ReadableStreamCreateReadResult(cx, UndefinedHandleValue, true,
+                                                       forAuthorCode);
             if (!resultObj) {
                 return false;
             }
             resultVal = ObjectValue(*resultObj);
             if (!ResolvePromise(cx, readRequest, resultVal)) {
                 return false;
             }
         }
@@ -1472,16 +1491,50 @@ ReadableStreamCloseInternal(JSContext* c
                                                     source,
                                                     unwrappedStream->embeddingFlags());
     }
 
     return true;
 }
 
 /**
+ * Streams spec, 3.4.5. ReadableStreamCreateReadResult ( value, done, forAuthorCode )
+ */
+static MOZ_MUST_USE JSObject*
+ReadableStreamCreateReadResult(JSContext* cx,
+                               HandleValue value,
+                               bool done,
+                               ForAuthorCodeBool forAuthorCode)
+{
+    // Step 1: Let prototype be null.
+    // Step 2: If forAuthorCode is true, set prototype to %ObjectPrototype%.
+    RootedObject templateObject(cx,
+        forAuthorCode == ForAuthorCodeBool::Yes
+        ? cx->realm()->getOrCreateIterResultTemplateObject(cx)
+        : cx->realm()->getOrCreateIterResultWithoutPrototypeTemplateObject(cx));
+
+    // Step 3: Assert: Type(done) is Boolean (implicit).
+
+    // Step 4: Let obj be ObjectCreate(prototype).
+    NativeObject* obj;
+    JS_TRY_VAR_OR_RETURN_NULL(cx, obj, NativeObject::createWithTemplate(cx, gc::DefaultHeap,
+                                                                        templateObject));
+
+    // Step 5: Perform CreateDataProperty(obj, "value", value).
+    obj->setSlot(Realm::IterResultObjectValueSlot, value);
+
+    // Step 6: Perform CreateDataProperty(obj, "done", done).
+    obj->setSlot(Realm::IterResultObjectDoneSlot,
+                 done ? TrueHandleValue : FalseHandleValue);
+
+    // Step 7: Return obj.
+    return obj;
+}
+
+/**
  * Streams spec, 3.4.6. ReadableStreamError ( stream, e )
  */
 MOZ_MUST_USE bool
 ReadableStreamErrorInternal(JSContext* cx, Handle<ReadableStream*> unwrappedStream, HandleValue e)
 {
     // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
 
     // Step 2: Assert: stream.[[state]] is "readable".
@@ -1605,19 +1658,20 @@ ReadableStreamFulfillReadOrReadIntoReque
     //         and so on).
     RootedNativeObject unwrappedReadIntoRequests(cx, unwrappedReader->requests());
     RootedObject readIntoRequest(cx, ShiftFromList<JSObject>(cx, unwrappedReadIntoRequests));
     MOZ_ASSERT(readIntoRequest);
     if (!cx->compartment()->wrap(cx, &readIntoRequest)) {
         return false;
     }
 
-    // Step 4: Resolve readIntoRequest.[[promise]] with
-    //         ! CreateIterResultObject(chunk, done).
-    RootedObject iterResult(cx, CreateIterResultObject(cx, chunk, done));
+    // Step 4: Resolve read{Into}Request.[[promise]] with
+    //         ! ReadableStreamCreateReadResult(chunk, done, readIntoRequest.[[forAuthorCode]]).
+    RootedObject iterResult(cx,
+        ReadableStreamCreateReadResult(cx, chunk, done, unwrappedReader->forAuthorCode()));
     if (!iterResult) {
         return false;
     }
     RootedValue val(cx, ObjectValue(*iterResult));
     return ResolvePromise(cx, readIntoRequest, val);
 }
 
 /**
@@ -1672,40 +1726,43 @@ ReadableStreamHasDefaultReader(JSContext
 }
 
 
 /*** 3.5. Class ReadableStreamDefaultReader ******************************************************/
 
 static MOZ_MUST_USE bool
 ReadableStreamReaderGenericInitialize(JSContext* cx,
                                       Handle<ReadableStreamReader*> reader,
-                                      Handle<ReadableStream*> unwrappedStream);
+                                      Handle<ReadableStream*> unwrappedStream,
+                                      ForAuthorCodeBool forAuthorCode);
 
 /**
  * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
  * Steps 2-4.
  */
 static MOZ_MUST_USE ReadableStreamDefaultReader*
-CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> unwrappedStream)
+CreateReadableStreamDefaultReader(JSContext* cx,
+                                  Handle<ReadableStream*> unwrappedStream,
+                                  ForAuthorCodeBool forAuthorCode)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx,
         NewBuiltinClassInstance<ReadableStreamDefaultReader>(cx));
     if (!reader) {
         return nullptr;
     }
 
     // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
     //         exception.
     if (unwrappedStream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
         return nullptr;
     }
 
     // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
-    if (!ReadableStreamReaderGenericInitialize(cx, reader, unwrappedStream)) {
+    if (!ReadableStreamReaderGenericInitialize(cx, reader, unwrappedStream, forAuthorCode)) {
         return nullptr;
     }
 
     // Step 4: Set this.[[readRequests]] to a new empty List.
     if (!SetNewList(cx, reader, ReadableStreamReader::Slot_Requests)) {
         return nullptr;
     }
 
@@ -1826,17 +1883,17 @@ ReadableStreamDefaultReader_read(JSConte
     // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
     //         rejected with a TypeError exception.
     if (!unwrappedReader->hasStream()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
-    // Step 3: Return ! ReadableStreamDefaultReaderRead(this).
+    // Step 3: Return ! ReadableStreamDefaultReaderRead(this, true).
     JSObject* readPromise = ::ReadableStreamDefaultReaderRead(cx, unwrappedReader);
     if (!readPromise) {
         return false;
     }
     args.rval().setObject(*readPromise);
     return true;
 }
 
@@ -1932,18 +1989,20 @@ ReadableStreamReaderGenericCancel(JSCont
     // Step 3: Return ! ReadableStreamCancel(stream, reason).
     return ::ReadableStreamCancel(cx, unwrappedStream, reason);
 }
 
 /**
  * Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
  */
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericInitialize(JSContext* cx, Handle<ReadableStreamReader*> reader,
-                                      Handle<ReadableStream*> unwrappedStream)
+ReadableStreamReaderGenericInitialize(JSContext* cx,
+                                      Handle<ReadableStreamReader*> reader,
+                                      Handle<ReadableStream*> unwrappedStream,
+                                      ForAuthorCodeBool forAuthorCode)
 {
     cx->check(reader);
 
     // Step 1: Set reader.[[ownerReadableStream]] to stream.
     {
         RootedObject readerCompartmentStream(cx, unwrappedStream);
         if (!cx->compartment()->wrap(cx, &readerCompartmentStream)) {
             return false;
@@ -1986,16 +2045,21 @@ ReadableStreamReaderGenericInitialize(JS
         promise = PromiseObject::unforgeableReject(cx, storedError);
     }
 
     if (!promise) {
         return false;
     }
 
     reader->setClosedPromise(promise);
+
+    // Extra step not in the standard. See the comment on
+    // `ReadableStreamReader::forAuthorCode()`.
+    reader->setForAuthorCode(forAuthorCode);
+
     return true;
 }
 
 /**
  * Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
  */
 static MOZ_MUST_USE bool
 ReadableStreamReaderGenericRelease(JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader)
@@ -2061,57 +2125,61 @@ ReadableStreamReaderGenericRelease(JSCon
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
 ReadableStreamControllerPullSteps(JSContext* cx,
                                   Handle<ReadableStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader )
+ * Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader [, forAuthorCode ] )
  */
 static MOZ_MUST_USE JSObject*
 ReadableStreamDefaultReaderRead(JSContext* cx,
                                 Handle<ReadableStreamDefaultReader*> unwrappedReader)
 {
-    // Step 1: Let stream be reader.[[ownerReadableStream]].
-    // Step 2: Assert: stream is not undefined.
+    // Step 1: If forAuthorCode was not passed, set it to false (implicit).
+
+    // Step 2: Let stream be reader.[[ownerReadableStream]].
+    // Step 3: Assert: stream is not undefined.
     Rooted<ReadableStream*> unwrappedStream(cx, UnwrapStreamFromReader(cx, unwrappedReader));
     if (!unwrappedStream) {
         return nullptr;
     }
 
-    // Step 3: Set stream.[[disturbed]] to true.
+    // Step 4: Set stream.[[disturbed]] to true.
     unwrappedStream->setDisturbed();
 
-    // Step 4: If stream.[[state]] is "closed", return a new promise resolved with
-    //         ! CreateIterResultObject(undefined, true).
+    // Step 5: If stream.[[state]] is "closed", return a new promise resolved with
+    //         ! ReadableStreamCreateReadResult(undefined, true, forAuthorCode).
     if (unwrappedStream->closed()) {
-        RootedObject iterResult(cx, CreateIterResultObject(cx, UndefinedHandleValue, true));
+        RootedObject iterResult(cx,
+            ReadableStreamCreateReadResult(cx, UndefinedHandleValue, true,
+                                           unwrappedReader->forAuthorCode()));
         if (!iterResult) {
             return nullptr;
         }
         RootedValue iterResultVal(cx, ObjectValue(*iterResult));
         return PromiseObject::unforgeableResolve(cx, iterResultVal);
     }
 
-    // Step 5: If stream.[[state]] is "errored", return a new promise rejected with
+    // Step 6: If stream.[[state]] is "errored", return a new promise rejected with
     //         stream.[[storedError]].
     if (unwrappedStream->errored()) {
         RootedValue storedError(cx, unwrappedStream->storedError());
         if (!cx->compartment()->wrap(cx, &storedError)) {
             return nullptr;
         }
         return PromiseObject::unforgeableReject(cx, storedError);
     }
 
-    // Step 6: Assert: stream.[[state]] is "readable".
+    // Step 7: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(unwrappedStream->readable());
 
-    // Step 7: Return ! stream.[[readableStreamController]].[[PullSteps]]().
+    // Step 8: Return ! stream.[[readableStreamController]].[[PullSteps]]().
     Rooted<ReadableStreamController*> unwrappedController(cx, unwrappedStream->controller());
     return ReadableStreamControllerPullSteps(cx, unwrappedController);
 }
 
 
 /*** 3.8. Class ReadableStreamDefaultController **************************************************/
 
 inline static MOZ_MUST_USE bool
@@ -2575,17 +2643,17 @@ ReadableStreamControllerCancelSteps(JSCo
 }
 
 inline static MOZ_MUST_USE bool
 DequeueValue(JSContext* cx,
              Handle<ReadableStreamController*> unwrappedContainer,
              MutableHandleValue chunk);
 
 /**
- * Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
+ * Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]( forAuthorCode )
  */
 static JSObject*
 ReadableStreamDefaultControllerPullSteps(JSContext* cx,
                                          Handle<ReadableStreamDefaultController*> unwrappedController)
 {
     // Step 1: Let stream be this.[[controlledReadableStream]].
     Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
@@ -2615,27 +2683,34 @@ ReadableStreamDefaultControllerPullSteps
 
         // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
         else {
             if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) {
                 return nullptr;
             }
         }
 
-        // Step d: Return a promise resolved with ! CreateIterResultObject(chunk, false).
+        // Step d: Return a promise resolved with
+        //         ! ReadableStreamCreateReadResult(chunk, false, forAuthorCode).
         cx->check(chunk);
-        RootedObject iterResultObj(cx, CreateIterResultObject(cx, chunk, false));
-        if (!iterResultObj) {
+        ReadableStreamReader* unwrappedReader = UnwrapReaderFromStream(cx, unwrappedStream);
+        if (!unwrappedReader) {
             return nullptr;
         }
-        RootedValue iterResult(cx, ObjectValue(*iterResultObj));
-        return PromiseObject::unforgeableResolve(cx, iterResult);
-    }
-
-    // Step 3: Let pendingPromise be ! ReadableStreamAddReadRequest(stream).
+        RootedObject readResultObj(cx,
+            ReadableStreamCreateReadResult(cx, chunk, false, unwrappedReader->forAuthorCode()));
+        if (!readResultObj) {
+            return nullptr;
+        }
+        RootedValue readResult(cx, ObjectValue(*readResultObj));
+        return PromiseObject::unforgeableResolve(cx, readResult);
+    }
+
+    // Step 3: Let pendingPromise be
+    //         ! ReadableStreamAddReadRequest(stream, forAuthorCode).
     RootedObject pendingPromise(cx, ReadableStreamAddReadOrReadIntoRequest(cx, unwrappedStream));
     if (!pendingPromise) {
         return nullptr;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
     if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) {
         return nullptr;
@@ -3292,17 +3367,17 @@ CLASS_SPEC(ReadableByteStreamController,
 // Streams spec, 3.10.5.1. [[CancelSteps]] ()
 // Unified with 3.8.5.1 above.
 
 static MOZ_MUST_USE bool
 ReadableByteStreamControllerHandleQueueDrain(JSContext* cx,
                                              Handle<ReadableStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.10.5.2. [[PullSteps]] ()
+ * Streams spec, 3.10.5.2. [[PullSteps]] ( forAuthorCode )
  */
 static MOZ_MUST_USE JSObject*
 ReadableByteStreamControllerPullSteps(JSContext* cx,
                                       Handle<ReadableByteStreamController*> unwrappedController)
 {
     // Step 1: Let stream be this.[[controlledReadableStream]].
     Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
@@ -3381,23 +3456,29 @@ ReadableByteStreamControllerPullSteps(JS
         unwrappedController->setQueueTotalSize(queueTotalSize);
 
         // Step 3.e: Perform ! ReadableByteStreamControllerHandleQueueDrain(this).
         // (reordered)
         if (!ReadableByteStreamControllerHandleQueueDrain(cx, unwrappedController)) {
             return nullptr;
         }
 
-        // Step 3.g: Return a promise resolved with ! CreateIterResultObject(view, false).
+        // Step 3.g: Return a promise resolved with
+        //           ! ReadableStreamCreateReadResult(view, false, forAuthorCode).
         val.setObject(*view);
-        RootedObject iterResult(cx, CreateIterResultObject(cx, val, false));
-        if (!iterResult) {
+        ReadableStreamReader* unwrappedReader = UnwrapReaderFromStream(cx, unwrappedStream);
+        if (!unwrappedReader) {
             return nullptr;
         }
-        val.setObject(*iterResult);
+        RootedObject readResult(cx,
+            ReadableStreamCreateReadResult(cx, val, false, unwrappedReader->forAuthorCode()));
+        if (!readResult) {
+            return nullptr;
+        }
+        val.setObject(*readResult);
 
         return PromiseObject::unforgeableResolve(cx, val);
     }
 
     // Step 4: Let autoAllocateChunkSize be this.[[autoAllocateChunkSize]].
     val = unwrappedController->autoAllocateChunkSize();
 
     // Step 5: If autoAllocateChunkSize is not undefined,
@@ -3435,17 +3516,17 @@ ReadableByteStreamControllerPullSteps(JS
                                 unwrappedController,
                                 ReadableByteStreamController::Slot_PendingPullIntos,
                                 pullIntoDescriptor))
         {
             return nullptr;
         }
     }
 
-    // Step 6: Let promise be ! ReadableStreamAddReadRequest(stream).
+    // Step 6: Let promise be ! ReadableStreamAddReadRequest(stream, forAuthorCode).
     RootedObject promise(cx, ReadableStreamAddReadOrReadIntoRequest(cx, unwrappedStream));
     if (!promise) {
         return nullptr;
     }
 
     // Step 7: Perform ! ReadableByteStreamControllerCallPullIfNeeded(this).
     if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) {
         return nullptr;
@@ -3453,19 +3534,19 @@ ReadableByteStreamControllerPullSteps(JS
 
     // Step 8: Return promise.
     return promise;
 }
 
 /**
  * Unified implementation of ReadableStream controllers' [[PullSteps]] internal
  * methods.
- * Streams spec, 3.8.5.2. [[PullSteps]] ()
+ * Streams spec, 3.8.5.2. [[PullSteps]] ( forAuthorCode )
  * and
- * Streams spec, 3.10.5.2. [[PullSteps]] ()
+ * Streams spec, 3.10.5.2. [[PullSteps]] ( forAuthorCode )
  */
 static MOZ_MUST_USE JSObject*
 ReadableStreamControllerPullSteps(JSContext* cx, Handle<ReadableStreamController*> unwrappedController)
 {
     if (unwrappedController->is<ReadableStreamDefaultController>()) {
         Rooted<ReadableStreamDefaultController*> unwrappedDefaultController(cx,
             &unwrappedController->as<ReadableStreamDefaultController>());
         return ReadableStreamDefaultControllerPullSteps(cx, unwrappedDefaultController);
@@ -4512,30 +4593,35 @@ JS::ReadableStreamReaderCancel(JSContext
     CHECK_THREAD(cx);
     cx->check(reason);
 
     Rooted<ReadableStreamReader*> unwrappedReader(cx,
         APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
     if (!unwrappedReader) {
         return false;
     }
+    MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
+               "C++ code should not touch readers created by scripts");
 
     return ReadableStreamReaderGenericCancel(cx, unwrappedReader, reason);
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStreamReader*> unwrappedReader(cx, APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
+    Rooted<ReadableStreamReader*> unwrappedReader(cx,
+        APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
     if (!unwrappedReader) {
         return false;
     }
+    MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
+               "C++ code should not touch readers created by scripts");
 
 #ifdef DEBUG
     Rooted<ReadableStream*> unwrappedStream(cx, UnwrapStreamFromReader(cx, unwrappedReader));
     if (!unwrappedStream) {
         return false;
     }
     MOZ_ASSERT(ReadableStreamGetNumReadRequests(unwrappedStream) == 0);
 #endif // DEBUG
@@ -4549,11 +4635,13 @@ JS::ReadableStreamDefaultReaderRead(JSCo
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
     Rooted<ReadableStreamDefaultReader*> unwrappedReader(cx);
     unwrappedReader = APIToUnwrapped<ReadableStreamDefaultReader>(cx, readerObj);
     if (!unwrappedReader) {
         return nullptr;
     }
+    MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
+               "C++ code should not touch readers created by scripts");
 
     return ::ReadableStreamDefaultReaderRead(cx, unwrappedReader);
 }
--- a/js/src/builtin/Stream.h
+++ b/js/src/builtin/Stream.h
@@ -110,16 +110,23 @@ class ReadableStream : public NativeObje
   public:
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
+/**
+ * Tells whether or not read() result objects inherit from Object.prototype.
+ * Generally, they should do so only if the reader was created by author code.
+ * See <https://streams.spec.whatwg.org/#readable-stream-create-read-result>.
+ */
+enum class ForAuthorCodeBool { No, Yes };
+
 class ReadableStreamReader : public NativeObject
 {
   public:
     /**
      * Memory layout of Stream Reader instances.
      *
      * See https://streams.spec.whatwg.org/#default-reader-internal-slots and
      * https://streams.spec.whatwg.org/#byob-reader-internal-slots for details.
@@ -138,24 +145,47 @@ class ReadableStreamReader : public Nati
      *
      * Requests is guaranteed to be in the same compartment as the Reader, but
      * can contain wrapped request objects from other globals.
      */
     enum Slots {
         Slot_Stream,
         Slot_Requests,
         Slot_ClosedPromise,
+        Slot_ForAuthorCode,
         SlotCount,
     };
 
     bool hasStream() const { return !getFixedSlot(Slot_Stream).isUndefined(); }
     void setStream(JSObject* stream) { setFixedSlot(Slot_Stream, ObjectValue(*stream)); }
     void clearStream() { setFixedSlot(Slot_Stream, UndefinedValue()); }
     bool isClosed() { return !hasStream(); }
 
+    /**
+     * Tells whether this reader was created by author code.
+     *
+     * This returns Yes for readers created using `stream.getReader()`, and No
+     * for readers created for the internal use of algorithms like
+     * `stream.tee()` and `new Response(stream)`.
+     *
+     * The standard does not have this field. Instead, eight algorithms take a
+     * forAuthorCode parameter, and a [[forAuthorCode]] field is part of each
+     * read request. But the behavior is always equivalent to treating readers
+     * created by author code as having a bit set on them. We implement it that
+     * way for simplicity.
+     */
+    ForAuthorCodeBool forAuthorCode() const {
+        return getFixedSlot(Slot_ForAuthorCode).toBoolean()
+               ? ForAuthorCodeBool::Yes
+               : ForAuthorCodeBool::No;
+    }
+    void setForAuthorCode(ForAuthorCodeBool value) {
+        setFixedSlot(Slot_ForAuthorCode, BooleanValue(value == ForAuthorCodeBool::Yes));
+    }
+
     NativeObject* requests() const {
         return &getFixedSlot(Slot_Requests).toObject().as<NativeObject>();
     }
     void clearRequests() { setFixedSlot(Slot_Requests, UndefinedValue()); }
 
     JSObject* closedPromise() const { return &getFixedSlot(Slot_ClosedPromise).toObject(); }
     void setClosedPromise(JSObject* wrappedPromise) {
         setFixedSlot(Slot_ClosedPromise, ObjectValue(*wrappedPromise));
--- a/js/src/vm/Iteration.cpp
+++ b/js/src/vm/Iteration.cpp
@@ -1031,44 +1031,69 @@ NativeObject*
 Realm::getOrCreateIterResultTemplateObject(JSContext* cx)
 {
     MOZ_ASSERT(cx->realm() == this);
 
     if (iterResultTemplate_) {
         return iterResultTemplate_;
     }
 
+    NativeObject* templateObj = createIterResultTemplateObject(cx, WithObjectPrototype::Yes);
+    iterResultTemplate_.set(templateObj);
+    return iterResultTemplate_;
+}
+
+NativeObject*
+Realm::getOrCreateIterResultWithoutPrototypeTemplateObject(JSContext* cx)
+{
+    MOZ_ASSERT(cx->realm() == this);
+
+    if (iterResultWithoutPrototypeTemplate_) {
+        return iterResultWithoutPrototypeTemplate_;
+    }
+
+    NativeObject* templateObj = createIterResultTemplateObject(cx, WithObjectPrototype::No);
+    iterResultWithoutPrototypeTemplate_.set(templateObj);
+    return iterResultWithoutPrototypeTemplate_;
+}
+
+NativeObject*
+Realm::createIterResultTemplateObject(JSContext* cx, WithObjectPrototype withProto)
+{
     // Create template plain object
-    RootedNativeObject templateObject(cx, NewBuiltinClassInstance<PlainObject>(cx, TenuredObject));
+    RootedNativeObject templateObject(cx,
+        withProto == WithObjectPrototype::Yes
+        ? NewBuiltinClassInstance<PlainObject>(cx, TenuredObject)
+        : NewObjectWithNullTaggedProto<PlainObject>(cx));
     if (!templateObject) {
-        return iterResultTemplate_; // = nullptr
+        return nullptr;
     }
 
     // Create a new group for the template.
     Rooted<TaggedProto> proto(cx, templateObject->taggedProto());
     RootedObjectGroup group(cx, ObjectGroupRealm::makeGroup(cx, templateObject->realm(),
                                                             templateObject->getClass(),
                                                             proto));
     if (!group) {
-        return iterResultTemplate_; // = nullptr
+        return nullptr;
     }
     templateObject->setGroup(group);
 
     // Set dummy `value` property
     if (!NativeDefineDataProperty(cx, templateObject, cx->names().value, UndefinedHandleValue,
                                   JSPROP_ENUMERATE))
     {
-        return iterResultTemplate_; // = nullptr
+        return nullptr;
     }
 
     // Set dummy `done` property
     if (!NativeDefineDataProperty(cx, templateObject, cx->names().done, TrueHandleValue,
                                   JSPROP_ENUMERATE))
     {
-        return iterResultTemplate_; // = nullptr
+        return nullptr;
     }
 
     AutoSweepObjectGroup sweep(group);
     if (!group->unknownProperties(sweep)) {
         // Update `value` property typeset, since it can be any value.
         HeapTypeSet* types = group->maybeGetProperty(sweep, NameToId(cx->names().value));
         MOZ_ASSERT(types);
         {
@@ -1079,19 +1104,17 @@ Realm::getOrCreateIterResultTemplateObje
 
     // Make sure that the properties are in the right slots.
     DebugOnly<Shape*> shape = templateObject->lastProperty();
     MOZ_ASSERT(shape->previous()->slot() == Realm::IterResultObjectValueSlot &&
                shape->previous()->propidRef() == NameToId(cx->names().value));
     MOZ_ASSERT(shape->slot() == Realm::IterResultObjectDoneSlot &&
                shape->propidRef() == NameToId(cx->names().done));
 
-    iterResultTemplate_.set(templateObject);
-
-    return iterResultTemplate_;
+    return templateObject;
 }
 
 /*** Iterator objects ****************************************************************************/
 
 size_t
 PropertyIteratorObject::sizeOfMisc(mozilla::MallocSizeOf mallocSizeOf) const
 {
     return mallocSizeOf(getPrivate());
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -525,16 +525,22 @@ Realm::sweepTemplateObjects()
 
     if (unmappedArgumentsTemplate_ && IsAboutToBeFinalized(&unmappedArgumentsTemplate_)) {
         unmappedArgumentsTemplate_.set(nullptr);
     }
 
     if (iterResultTemplate_ && IsAboutToBeFinalized(&iterResultTemplate_)) {
         iterResultTemplate_.set(nullptr);
     }
+
+    if (iterResultWithoutPrototypeTemplate_ &&
+        IsAboutToBeFinalized(&iterResultWithoutPrototypeTemplate_))
+    {
+        iterResultWithoutPrototypeTemplate_.set(nullptr);
+    }
 }
 
 void
 Realm::fixupAfterMovingGC()
 {
     purge();
     fixupGlobal();
     objectGroups_.fixupTablesAfterMovingGC();
--- a/js/src/vm/Realm.h
+++ b/js/src/vm/Realm.h
@@ -360,16 +360,17 @@ class JS::Realm : public JS::shadow::Rea
     // This pointer is controlled by the embedder. If it is non-null, and if
     // cx->enableAccessValidation is true, then we assert that *validAccessPtr
     // is true before running any code in this realm.
     bool* validAccessPtr_ = nullptr;
 
     js::ReadBarriered<js::ArgumentsObject*> mappedArgumentsTemplate_ { nullptr };
     js::ReadBarriered<js::ArgumentsObject*> unmappedArgumentsTemplate_ { nullptr };
     js::ReadBarriered<js::NativeObject*> iterResultTemplate_ { nullptr };
+    js::ReadBarriered<js::NativeObject*> iterResultWithoutPrototypeTemplate_ { nullptr };
 
     // There are two ways to enter a realm:
     //
     // (1) AutoRealm (and JSAutoRealm, JS::EnterRealm)
     // (2) When calling a cross-realm (but same-compartment) function in JIT
     //     code.
     //
     // This field only accounts for (1), to keep the JIT code as simple as
@@ -690,17 +691,23 @@ class JS::Realm : public JS::shadow::Rea
     // Used to approximate non-content code when reporting telemetry.
     bool isProbablySystemCode() const {
         return isSystem_;
     }
 
     static const size_t IterResultObjectValueSlot = 0;
     static const size_t IterResultObjectDoneSlot = 1;
     js::NativeObject* getOrCreateIterResultTemplateObject(JSContext* cx);
+    js::NativeObject* getOrCreateIterResultWithoutPrototypeTemplateObject(JSContext* cx);
 
+  private:
+    enum class WithObjectPrototype { No, Yes };
+    js::NativeObject* createIterResultTemplateObject(JSContext* cx, WithObjectPrototype withProto);
+
+  public:
     js::ArgumentsObject* getOrCreateArgumentsTemplateObject(JSContext* cx, bool mapped);
     js::ArgumentsObject* maybeArgumentsTemplateObject(bool mapped) const;
 
     //
     // The Debugger observes execution on a frame-by-frame basis. The
     // invariants of Realm's debug mode bits, JSScript::isDebuggee,
     // InterpreterFrame::isDebuggee, and BaselineFrame::isDebuggee are
     // enumerated below.
--- a/testing/web-platform/meta/fetch/api/response/response-stream-with-broken-then.any.js.ini
+++ b/testing/web-platform/meta/fetch/api/response/response-stream-with-broken-then.any.js.ini
@@ -1,33 +1,9 @@
 [response-stream-with-broken-then.any.html]
-  [Attempt to inject undefined via Object.prototype.then.]
-    expected: FAIL
-
-  [Attempt to inject {done: false, value: bye} via Object.prototype.then.]
-    expected: FAIL
-
   [intercepting arraybuffer to body readable stream conversion via Object.prototype.then should not be possible]
     expected: FAIL
 
-  [Attempt to inject 8.2 via Object.prototype.then.]
-    expected: FAIL
-
-  [Attempt to inject value: undefined via Object.prototype.then.]
-    expected: FAIL
-
 
 [response-stream-with-broken-then.any.worker.html]
-  [Attempt to inject undefined via Object.prototype.then.]
-    expected: FAIL
-
-  [Attempt to inject {done: false, value: bye} via Object.prototype.then.]
-    expected: FAIL
-
   [intercepting arraybuffer to body readable stream conversion via Object.prototype.then should not be possible]
     expected: FAIL
 
-  [Attempt to inject 8.2 via Object.prototype.then.]
-    expected: FAIL
-
-  [Attempt to inject value: undefined via Object.prototype.then.]
-    expected: FAIL
-