Closed Bug 2065919 Opened 2 months ago Closed 2 months ago

Crash in `WritableStreamDefaultWriterWrite` via `ReadableStream.pipeTo()` shutdown ordering

Categories

(Core :: DOM: Streams, defect)

defect

Tracking

()

RESOLVED FIXED
157 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox-esr153 --- fixed
firefox155 --- wontfix
firefox156 --- wontfix
firefox157 --- fixed

People

(Reporter: phambao1340, Assigned: saschanaz)

References

(Regression)

Details

(4 keywords, Whiteboard: [affects Chrome also][client-bounty-form], [wptsync upstream])

Crash Data

Attachments

(2 external files)

Component: Core :: DOM: Streams
Files: dom/streams/ReadableStreamPipeTo.cpp, dom/streams/WritableStreamDefaultWriter.cpp
Tested build: Firefox 156.0a1, 22598deb7560ad7baef8c8f5082dcfc1e92efd54, ASan opt (--enable-address-sanitizer --disable-debug --enable-optimize)
Reproduces: deterministically, first attempt, every run. Main thread and DedicatedWorker.


1. Summary

PipeToPump::OnReadFulfilled defers each chunk write into a promise microtask. The lambda body
calls the internal abstract operation WritableStreamDefaultWriterWrite without re-checking any
state, even though a full pipe shutdown β€” including WritableStreamDefaultWriterRelease, which
clears writer.[[stream]] β€” can complete in the microtask that runs immediately before it.

WritableStreamDefaultWriterWrite transcribes the spec's Assert: stream is not undefined as a
MOZ_ASSERT and then immediately dereferences stream. In an opt build the assert is compiled
out and the dereference faults.

The mShuttingDown guard that protects this path (ReadableStreamPipeTo.cpp:604) is evaluated
one microtask too early β€” before the shutdown that invalidates the writer has run.


2. The code

2.1 The deferred write β€” dom/streams/ReadableStreamPipeTo.cpp:602-630

  if (mShuttingDown) {          // :604  <-- guard evaluated HERE
    return;
  }

  // Write asynchronously. Roughly this is like:
  // `Promise.resolve().then(() => stream.write(chunk));`
  // XXX: The spec currently does not require asynchronicity, but this still
  // matches other engines' behavior. See
  // https://github.com/whatwg/streams/issues/1243.
  RefPtr<Promise> promise =
      Promise::CreateInfallible(xpc::CurrentNativeGlobal(aCx));
  promise->MaybeResolveWithUndefined();
  auto result = promise->ThenWithCycleCollectedArgsJS(
      [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv,
         const RefPtr<PipeToPump>& aSelf,
         const RefPtr<WritableStreamDefaultWriter>& aWriter,
         JS::Handle<JS::Value> aChunk)
          MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> already_AddRefed<Promise> {
            RefPtr<Promise> promise =
                WritableStreamDefaultWriterWrite(aCx, aWriter, aChunk, aRv);  // :623  <-- used HERE
            // Last read has finished, so it's time to start reading again.
            aSelf->Read(aCx);
            return promise.forget();
          },
      std::make_tuple(RefPtr{this}, mWriter), std::make_tuple(aChunk));

The lambda captures its own RefPtr to the writer, so the writer object is guaranteed to stay
alive. What is not guaranteed is that the writer is still attached to a stream.

2.2 The unguarded dereference β€” dom/streams/WritableStreamDefaultWriter.cpp:270-280

already_AddRefed<Promise> WritableStreamDefaultWriterWrite(
    JSContext* aCx, WritableStreamDefaultWriter* aWriter,
    JS::Handle<JS::Value> aChunk, ErrorResult& aRv) {
  // Step 1. Let stream be writer.[[stream]].
  RefPtr<WritableStream> stream = aWriter->GetStream();

  // Step 2. Assert: stream is not undefined.
  MOZ_ASSERT(stream);                                          // :277  compiled out in opt

  // Step 3. Let controller be stream.[[controller]].
  RefPtr<WritableStreamDefaultController> controller = stream->Controller();  // :279  FAULT

The JS-facing entry point does perform the real check; the internal one relies on the assert:

// dom/streams/WritableStreamDefaultWriter.cpp:352-356  (WritableStreamDefaultWriter::Write)
  // Step 1. If this.[[stream]] is undefined, return a promise rejected with a
  // TypeError exception.
  if (!mStream) {
    aRv.ThrowTypeError("Missing stream");
    return nullptr;
  }

2.3 Where the stream is cleared β€” dom/streams/WritableStreamDefaultWriter.cpp:241-245

  // Step 7. Set stream.[[writer]] to undefined.
  stream->SetWriter(nullptr);

  // Step 8. Set writer.[[stream]] to undefined.
  aWriter->SetStream(nullptr);                                 // :245

PipeToPump::Finalize calls this unconditionally as its first step
(ReadableStreamPipeTo.cpp:547-548).


3. Root cause

Three separate design decisions combine. None is wrong on its own.

3.1 Writes are deferred by one microtask, but the shutdown check is not

OnReadFulfilled intentionally defers the write to match other engines
(whatwg/streams#1243). The mShuttingDown test at :604 therefore describes the world as it was
when the chunk arrived, not the world the write actually executes in. Any shutdown that both
starts and completes in the interval between those two points is invisible to the pump.

3.2 The "wait for the last write" step only covers a writable destination

PipeToPump::ShutdownWithAction implements spec step 3 of shutdown with an action
(ReadableStreamPipeTo.cpp:409-428):

  // Step 3. If dest.[[state]] is "writable" and !
  // WritableStreamCloseQueuedOrInFlight(dest) is false,
  RefPtr<WritableStream> dest = mWriter->GetStream();
  if (dest->State() == WritableStream::WriterState::Writable &&
      !dest->CloseQueuedOrInFlight()) {
    // Step 3.1 / 3.2 ... wait until every chunk that has been read has been written
    if (mLastWritePromise) {
      mLastWritePromise->AppendNativeHandler(
          new WriteFinishedPromiseHandler(aCx, this, aAction, aError));
      return;                                    // <-- the protective path
    }
  }

  // Don't have to wait for last write, immediately continue.
  ShutdownWithActionAfterFinishedWrite(aCx, aAction, aError);

This is precisely the mechanism that would otherwise save the pending write: mLastWritePromise
is the promise returned by ThenWithCycleCollectedArgsJS, so it cannot settle until the deferred
lambda has run. Waiting on it orders Finalize strictly after the write.

But the wait is gated on dest->State() == Writable. When the destination is already errored,
the guard is skipped and the pending write is silently dropped from consideration β€” even though it
is still queued and will still execute.

This is faithful to the spec text, which only requires already-read chunks to be written when the
destination can still accept them. The spec, however, has no notion of a write that is already
in flight as a microtask; Gecko's asynchronous-write extension introduces exactly that state, and
step 3 was never widened to account for it.

3.3 A null action makes Finalize synchronous

ShutdownWithActionAfterFinishedWrite (ReadableStreamPipeTo.cpp:495-501):

void PipeToPump::ShutdownWithActionAfterFinishedWrite(
    JSContext* aCx, ShutdownAction aAction,
    JS::Handle<mozilla::Maybe<JS::Value>> aError) {
  if (!aAction) {
    // Used to implement shutdown without action. Finalize immediately.
    Finalize(aCx, aError);
    return;
  }
  ...
  p->AppendNativeHandler(new ShutdownActionFinishedPromiseHandler(aCx, this, aError));

PipeToPump::Shutdown is implemented as shutdown with a null action
(ReadableStreamPipeTo.cpp:534-539). With a non-null action, Finalize runs from a promise
reaction β€” at least one microtask later β€” which lands it after the pending write and is
harmless. With a null action, Finalize runs synchronously inside the shutdown reaction, ahead of
the pending write.

3.4 The resulting sequence

All of it happens inside a single task, in one microtask checkpoint:

task:
  wc.error(e)     WritableStreamDefaultControllerError
                    -> StartErroring -> FinishErroring (state = "errored")
                    -> RejectCloseAndClosedPromiseIfNeeded
                    -> writer.[[closedPromise]] rejected
                    => queue reaction A = PipeToPump::OnDestErrored

  rc.enqueue(v)   ReadableStreamDefaultControllerEnqueue
                    -> FulfillReadRequest -> PipeToReadRequest::ChunkSteps
                    -> PipeToPump::OnReadFulfilled
                         mShuttingDown == false   (A has not run yet)   <-- 3.1
                    => queue microtask M = deferred WritableStreamDefaultWriterWrite
                       mLastWritePromise = <promise settled by M>

microtask checkpoint (FIFO):
  A: OnDestErrored, mPreventCancel == true
       -> Shutdown(nullptr)
       -> ShutdownWithAction: mShuttingDown = true
                              dest->State() == Errored, not Writable      <-- 3.2
                              (mLastWritePromise is non-null but not consulted)
       -> ShutdownWithActionAfterFinishedWrite: aAction == nullptr        <-- 3.3
       -> Finalize
       -> WritableStreamDefaultWriterRelease -> writer.[[stream]] = nullptr

  M: WritableStreamDefaultWriterWrite(cx, writer, chunk, rv)
       stream = writer->GetStream()  ==  nullptr
       MOZ_ASSERT(stream)            compiled out
       stream->Controller()          READ at nullptr + offsetof(WritableStream, mController)

FinishErroring is gated on controller->Started() (dom/streams/WritableStream.cpp:409-412),
which is set from a microtask after construction. An error() issued in the same turn as
new WritableStream(...) therefore only reaches state erroring, never rejects the closed
promise, and queues nothing. The PoC must let at least one turn elapse first.


4. Proof of concept

Pure content JavaScript. No privileges, no user interaction, no non-default preferences.

poc.html:

<!doctype html>
<meta charset=utf-8>
<script>
let rc, wc;
const rs = new ReadableStream({ start(c) { rc = c; } });
const ws = new WritableStream({ start(c) { wc = c; }, write(chunk) {} });

// pipeTo() starts synchronously: the pump immediately issues a read that stays
// pending, and installs its OnDestErrored handler on the writer's closed promise.
rs.pipeTo(ws, { preventCancel: true }).catch(() => {});

// Wait one turn so the writable's controller reaches [[started]] = true.
setTimeout(() => {
  // 1. Errors the destination synchronously: state -> "errored", the writer's
  //    closed promise rejects now, queueing OnDestErrored as microtask A.
  wc.error(new Error("boom"));

  // 2. Synchronously fulfills the pump's pending read request. OnReadFulfilled
  //    sees mShuttingDown == false and queues the deferred write as microtask M,
  //    behind A.
  rc.enqueue("chunk");

  // Checkpoint: A releases the writer, then M writes through it.
}, 100);
</script>


5. ASan report

Content process, main thread. --disable-debug build, so MOZ_ASSERT at
WritableStreamDefaultWriter.cpp:277 is compiled out and execution proceeds to the dereference on
the next line.

[14:57:17] [ff]~/ff/firefox_lasted (main@22598deb75)#   mkdir -p /tmp/prof
  ASAN_OPTIONS=detect_leaks=0 LD_LIBRARY_PATH=$PWD/obj-asan/dist/bin \
    ./obj-asan/dist/bin/firefox -no-remote -profile /tmp/prof -headless \
    "file:///home/zx/ff/firefox_lasted/poc.html"
*** You are running in headless mode.
Crash Annotation GraphicsCriticalError: |[0][GFX1-]: RenderCompositorSWGL failed mapping default framebuffer, no dt (t=1.3412) [GFX1-]: RenderCompositorSWGL failed mapping default framebuffer, no dt
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1599232==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000048 (pc 0x7bd35edf7a51 bp 0x7ffd405420a0 sp 0x7ffd40541fe0 T0)
==1599232==The signal is caused by a READ memory access.
==1599232==Hint: address points to the zero page.
    #0 0x7bd35edf7a51 in get /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/RefPtr.h:314:27
    #1 0x7bd35edf7a51 in operator mozilla::dom::WritableStreamDefaultController * /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/RefPtr.h:327:12
    #2 0x7bd35edf7a51 in Controller /home/zx/ff/firefox_lasted/obj-asan/dom/streams/./../../../dom/streams/WritableStream.h:61:12
    #3 0x7bd35edf7a51 in mozilla::dom::streams_abstract::WritableStreamDefaultWriterWrite(JSContext*, mozilla::dom::WritableStreamDefaultWriter*, JS::Handle<JS::Value>, mozilla::ErrorResult&) /home/zx/ff/firefox_lasted/obj-asan/dom/streams/./../../../dom/streams/WritableStreamDefaultWriter.cpp:279:64
    #4 0x7bd35edbebec in operator() /home/zx/ff/firefox_lasted/obj-asan/dom/streams/./../../../dom/streams/ReadableStreamPipeTo.cpp:623:17
    #5 0x7bd35edbebec in CallCallback<(lambda at ./../../../dom/streams/ReadableStreamPipeTo.cpp:617:7), 0UL, 1UL, 0UL> /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/dom/Promise-inl.h:203:12
    #6 0x7bd35edbebec in already_AddRefed<mozilla::dom::Promise> mozilla::dom::promise_detail::NativeThenHandler<mozilla::dom::PipeToPump::OnReadFulfilled(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&)::$_0, mozilla::dom::PipeToPump::OnReadFulfilled(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&)::$_0, std::tuple<RefPtr<mozilla::dom::PipeToPump>, RefPtr<mozilla::dom::WritableStreamDefaultWriter>>, std::tuple<JS::Handle<JS::Value>>>::CallCallback<mozilla::dom::PipeToPump::OnReadFulfilled(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&)::$_0>(JSContext*, mozilla::dom::PipeToPump::OnReadFulfilled(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&)::$_0 const&, JS::Handle<JS::Value>, mozilla::ErrorResult&) /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/dom/Promise-inl.h:211:12
    #7 0x7bd35ed23572 in mozilla::dom::PromiseNativeThenHandlerBase::ResolvedCallback(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&) /home/zx/ff/firefox_lasted/obj-asan/dom/promise/./../../../dom/promise/Promise.cpp:377:29
    #8 0x7bd35ed2c82a in mozilla::dom::(anonymous namespace)::PromiseNativeHandlerShim::ResolvedCallback(JSContext*, JS::Handle<JS::Value>, mozilla::ErrorResult&) /home/zx/ff/firefox_lasted/obj-asan/dom/promise/./../../../dom/promise/Promise.cpp:561:12
    #9 0x7bd35ed2d386 in mozilla::dom::NativeHandlerCallback(JSContext*, unsigned int, JS::Value*) /home/zx/ff/firefox_lasted/obj-asan/dom/promise/./../../../dom/promise/Promise.cpp
    #10 0x7bd363ef253b in CallJSNative /home/zx/ff/firefox_lasted/js/src/vm/Interpreter.cpp:526:13
    #11 0x7bd363ef253b in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /home/zx/ff/firefox_lasted/js/src/vm/Interpreter.cpp:622:12
    #12 0x7bd363ef3c37 in InternalCall /home/zx/ff/firefox_lasted/js/src/vm/Interpreter.cpp:689:10
    #13 0x7bd363ef3c37 in js::Call(JSContext*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, js::AnyInvokeArgs const&, JS::MutableHandle<JS::Value>, js::CallReason) /home/zx/ff/firefox_lasted/js/src/vm/Interpreter.cpp:721:8
    #14 0x7bd3627fb04e in Call /home/zx/ff/firefox_lasted/js/src/vm/Interpreter.h:118:10
    #15 0x7bd3627fb04e in PromiseReactionJob /home/zx/ff/firefox_lasted/obj-asan/js/src/builtin/./../../../../js/src/builtin/Promise.cpp:2726:10
    #16 0x7bd3627fb04e in JS::RunJSMicroTask(JSContext*, JS::Handle<JSObject*>) /home/zx/ff/firefox_lasted/obj-asan/js/src/builtin/./../../../../js/src/builtin/Promise.cpp:8451:12
    #17 0x7bd3548f2133 in mozilla::MustConsumeMicroTask::RunAndConsumeJSMicroTask(JSContext*) /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/CycleCollectedJSContext.h:247:14
    #18 0x7bd3548a6c24 in RunJSMicroTask /home/zx/ff/firefox_lasted/obj-asan/xpcom/base/./../../../xpcom/base/CycleCollectedJSContext.cpp:1116:35
    #19 0x7bd3548a6c24 in mozilla::RunMicroTask(JSContext*, mozilla::CycleCollectedJSContext*, JS::MutableHandle<mozilla::MustConsumeMicroTask>, bool) /home/zx/ff/firefox_lasted/obj-asan/xpcom/base/./../../../xpcom/base/CycleCollectedJSContext.cpp:747:3
    #20 0x7bd3548a27de in mozilla::CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool) /home/zx/ff/firefox_lasted/obj-asan/xpcom/base/./../../../xpcom/base/CycleCollectedJSContext.cpp:1265:7
    #21 0x7bd35a172fa0 in LeaveMicroTask /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/CycleCollectedJSContext.h:462:7
    #22 0x7bd35a172fa0 in mozilla::dom::CallSetup::~CallSetup() /home/zx/ff/firefox_lasted/obj-asan/dom/bindings/./../../../dom/bindings/CallbackObject.cpp:489:11
    #23 0x7bd358113a38 in void mozilla::dom::Function::Call<nsCOMPtr<nsIGlobalObject>>(nsCOMPtr<nsIGlobalObject> const&, nsTArray<JS::Value> const&, JS::MutableHandle<JS::Value>, mozilla::ErrorResult&, char const*, mozilla::dom::CallbackObjectBase::ExceptionHandling, JS::Realm*) /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/dom/FunctionBinding.h:72:3
    #24 0x7bd3580fb7cf in mozilla::dom::CallbackTimeoutHandler::Call(char const*) /home/zx/ff/firefox_lasted/obj-asan/dom/base/./../../../dom/base/TimeoutHandler.cpp:167:29
    #25 0x7bd357c163d1 in nsGlobalWindowInner::RunTimeoutHandler(mozilla::dom::Timeout*) /home/zx/ff/firefox_lasted/dom/base/nsGlobalWindowInner.cpp:6604:38
    #26 0x7bd3580f77b2 in mozilla::dom::TimeoutManager::RunTimeout(mozilla::TimeStamp const&, mozilla::TimeStamp const&, bool) /home/zx/ff/firefox_lasted/obj-asan/dom/base/./../../../dom/base/TimeoutManager.cpp:955:39
    #27 0x7bd3580f6472 in mozilla::dom::TimeoutExecutor::MaybeExecute() /home/zx/ff/firefox_lasted/obj-asan/dom/base/./../../../dom/base/TimeoutExecutor.cpp:177:11
    #28 0x7bd3580f9007 in Notify /home/zx/ff/firefox_lasted/obj-asan/dom/base/./../../../dom/base/TimeoutExecutor.cpp:244:5
    #29 0x7bd3580f9007 in non-virtual thunk to mozilla::dom::TimeoutExecutor::Notify(nsITimer*) /home/zx/ff/firefox_lasted/obj-asan/dom/base/./../../../dom/base/TimeoutExecutor.cpp
    #30 0x7bd354b04313 in operator() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/nsTimerImpl.cpp:744:44
    #31 0x7bd354b04313 in matchN<mozilla::Variant<nsTimerImpl::UnknownCallback, nsCOMPtr<nsITimerCallback>, nsCOMPtr<nsIObserver>, nsTimerImpl::FuncCallback, std::function<void (nsITimer *)> > &, (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:744:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:745:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:748:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:749:7)> /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/Variant.h:327:16
    #32 0x7bd354b04313 in matchN<mozilla::Variant<nsTimerImpl::UnknownCallback, nsCOMPtr<nsITimerCallback>, nsCOMPtr<nsIObserver>, nsTimerImpl::FuncCallback, std::function<void (nsITimer *)> > &, (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:743:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:744:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:745:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:748:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:749:7)> /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/Variant.h:336:14
    #33 0x7bd354b04313 in matchN<mozilla::Variant<nsTimerImpl::UnknownCallback, nsCOMPtr<nsITimerCallback>, nsCOMPtr<nsIObserver>, nsTimerImpl::FuncCallback, std::function<void (nsITimer *)> > &, (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:743:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:744:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:745:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:748:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:749:7)> /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/Variant.h:925:12
    #34 0x7bd354b04313 in match<(lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:743:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:744:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:745:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:748:7), (lambda at ./../../../xpcom/threads/nsTimerImpl.cpp:749:7)> /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/Variant.h:880:12
    #35 0x7bd354b04313 in nsTimerImpl::Fire(unsigned long) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/nsTimerImpl.cpp:742:22
    #36 0x7bd354b039ea in nsTimerEvent::Run() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TimerThread.cpp:492:11
    #37 0x7bd354b46c76 in mozilla::ThrottledEventQueue::Inner::ExecuteRunnable() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/ThrottledEventQueue.cpp:251:18
    #38 0x7bd354b40a0f in mozilla::ThrottledEventQueue::Inner::Executor::Run() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/ThrottledEventQueue.cpp:78:15
    #39 0x7bd354af66ba in mozilla::RunnableTask::Run() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:721:16
    #40 0x7bd354ad0c53 in mozilla::TaskController::RunTask(mozilla::Task*) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:212:19
    #41 0x7bd354ad7c73 in mozilla::TaskController::DoExecuteNextTaskOnlyMainThreadInternal(mozilla::detail::BaseAutoLock<mozilla::Mutex&> const&) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:1360:20
    #42 0x7bd354ad5708 in mozilla::TaskController::ExecuteNextTaskOnlyMainThreadInternal(mozilla::detail::BaseAutoLock<mozilla::Mutex&> const&) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:1183:15
    #43 0x7bd354ad5d36 in mozilla::TaskController::ProcessPendingMTTask(bool) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:657:36
    #44 0x7bd354ad8cc4 in operator() /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/TaskController.cpp:352:37
    #45 0x7bd354ad8cc4 in mozilla::detail::RunnableFunction<mozilla::TaskController::TaskController()::$_1>::Run() /home/zx/ff/firefox_lasted/xpcom/threads/nsThreadUtils.h:535:5
    #46 0x7bd354b19ccd in nsThread::ProcessNextEvent(bool, bool*) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/nsThread.cpp:1177:16
    #47 0x7bd354b23c21 in NS_ProcessNextEvent(nsIThread*, bool) /home/zx/ff/firefox_lasted/obj-asan/xpcom/threads/./../../../xpcom/threads/nsThreadUtils.cpp:471:32
    #48 0x7bd354d749b4 in mozilla::ipc::MessagePump::Run(base::MessagePump::Delegate*) /home/zx/ff/firefox_lasted/obj-asan/ipc/glue/./../../../ipc/glue/MessagePump.cpp:105:5
    #49 0x7bd354c42ef4 in RunInternal /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:371:10
    #50 0x7bd354c42ef4 in RunHandler /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:364:3
    #51 0x7bd354c42ef4 in MessageLoop::Run() /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:346:3
    #52 0x7bd35f8f3b6c in nsBaseAppShell::Run() /home/zx/ff/firefox_lasted/obj-asan/widget/./../../widget/nsBaseAppShell.cpp:151:27
    #53 0x7bd35fb42e4b in nsAppShell::Run() /home/zx/ff/firefox_lasted/obj-asan/widget/gtk/./../../../widget/gtk/nsAppShell.cpp:580:33
    #54 0x7bd362140ce5 in XRE_RunAppShell() /home/zx/ff/firefox_lasted/toolkit/xre/nsEmbedFunctions.cpp:644:20
    #55 0x7bd354c42ef4 in RunInternal /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:371:10
    #56 0x7bd354c42ef4 in RunHandler /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:364:3
    #57 0x7bd354c42ef4 in MessageLoop::Run() /home/zx/ff/firefox_lasted/obj-asan/ipc/chromium/./../../../ipc/chromium/src/base/message_loop.cc:346:3
    #58 0x7bd362140425 in XRE_InitChildProcess(int, char**, XREChildData const*) /home/zx/ff/firefox_lasted/toolkit/xre/nsEmbedFunctions.cpp:582:34
    #59 0x58b5eb8a506e in main /home/zx/ff/firefox_lasted/browser/app/nsBrowserApp.cpp:469:22
    #60 0x7fd3706ed574 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #61 0x7fd3706ed627 in __libc_start_main csu/../csu/libc-start.c:360:3
    #62 0x58b5eb7bc7b8 in _start (/home/zx/ff/firefox_lasted/obj-asan/dist/bin/firefox+0xb87b8) (BuildId: fb105414eecb55930e1dfb041f09ca11)

==1599232==Register values:
rax = 0x0000000000000009  rbx = 0x00007ffd40541fe0  rcx = 0xf3f8f8f8f2f2f8f8  rdx = 0x0000000000000004
rdi = 0x0000000000000048  rsi = 0x00000000000000f3  rbp = 0x00007ffd405420a0  rsp = 0x00007ffd40541fe0
 r8 = 0x0000000000000000   r9 = 0x00007fffffffff01  r10 = 0x00007fffffffff01  r11 = 0x0000000000000001
r12 = 0x00007c436f82db48  r13 = 0x00000f7a6dca95e0  r14 = 0x00000f886df05b69  r15 = 0x0000000000000000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/zx/ff/firefox_lasted/obj-asan/dist/include/mozilla/RefPtr.h:314:27 in get
==1599232==ABORTING

6. Suggested fix

The minimal correct change is to make the deferred lambda re-validate the state it captured a
microtask earlier β€” the mShuttingDown check at :604 needs a counterpart at the point of use:

--- a/dom/streams/ReadableStreamPipeTo.cpp
+++ b/dom/streams/ReadableStreamPipeTo.cpp
@@
        [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv,
           const RefPtr<PipeToPump>& aSelf,
           const RefPtr<WritableStreamDefaultWriter>& aWriter,
           JS::Handle<JS::Value> aChunk)
            MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> already_AddRefed<Promise> {
+              // Shutdown may have completed - including
+              // WritableStreamDefaultWriterRelease - in a microtask queued
+              // before this one. The check in OnReadFulfilled describes the
+              // state at read time, not at write time.
+              if (aSelf->mShuttingDown) {
+                return nullptr;
+              }
+
              RefPtr<Promise> promise =
                  WritableStreamDefaultWriterWrite(aCx, aWriter, aChunk, aRv);
Flags: sec-bounty?

Thank you for the report. This looks like a bug, but generally we don't consider a null deref crash to be a security issue.

Group: firefox-core-security → dom-core-security
Component: Security → DOM: Streams
Product: Firefox → Core
Severity: -- → S3

It seems this affects Chrome too. This may be a spec level issue.

One-line version repro:

data:text/html,<script>let rc, wc; const rs = new ReadableStream({ start(c) { rc = c; } }); const ws = new WritableStream({ start(c) { wc = c; }, write(chunk) {} }); rs.pipeTo(ws, { preventCancel: true }).catch(() => {}); setTimeout(() => { wc.error(new Error("boom")); rc.enqueue("chunk"); }, 100);</script>

This rating has been determined by our automated bug rating tool (currently under development). No validation or verification has been performed.

Rating: N/A

Explanation: Confirmed from source: the async write callback in PipeToPump::OnReadFulfilled (ReadableStreamPipeTo.cpp:613-638) invokes WritableStreamDefaultWriterWrite in a microtask without re-checking mShuttingDown or the writer's [[stream]]. When pipeTo shutdown ordering lets Finalize() -> WritableStreamDefaultWriterRelease() run first (it sets writer.[[stream]] to null via SetStream(nullptr)) before that pending write executes, the write's step-1 GetStream() returns null; MOZ_ASSERT(stream) fires in assertion builds and in release builds stream->Controller() reads mController off a null pointer. Impact: this is a plain null-pointer dereference (or diagnostic assert) - the writer object stays alive via the callback's captured RefPtr, so no use-after-free or memory corruption. Attack vector: web content using ReadableStream.pipeTo() with a destination whose state changes during a pending write. Reachability: reachable from ordinary web content, but only in the content process. A safe content-process null-deref crash is not a security issue; this is a legitimate stability bug (already S3) but carries no security impact.

opt nightly bp-9454f0bc-b849-430b-b29a-488650260826

There is no evidence this is exploitable in Firefox, but we don't know about Chrome so we'll keep this hidden for now

Flags: sec-bounty? → sec-bounty-
Whiteboard: [client-bounty-form] → [affects Chrome also][client-bounty-form]

Blink's crash is a release assertion, it's not a security issue. Let's open this.

Assignee: nobody → krosylight
Status: NEW → ASSIGNED

:saschanaz, please update all release status flags to reflect the affected/unaffected/disabled state correctly. Which branches (beta, release, and/or ESR) are affected by this flaw?

Flags: needinfo?(krosylight)

Set release status flags based on info from the regressing bug 1822675

Dear bot you did it yourself.

Flags: needinfo?(krosylight)
Group: dom-core-security
Flags: needinfo?(dveditz)
Attachment #9635472 - Attachment description: (secure) → Bug 2065919 - Check shutdown again before async write on read fulfill r=#dom-core
Pushed by krosylight@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/8f580a73d6b9 https://hg.mozilla.org/integration/autoland/rev/01a2a5bb2e63 Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug
Pushed by amarc@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/73db385bb2d4 https://hg.mozilla.org/integration/autoland/rev/d61cc4782572 Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing lint failure @ shutdown.html

Backed out for causing lint failure

dt failure

Flags: needinfo?(krosylight)
Pushed by krosylight@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/d4bea9f805fc https://hg.mozilla.org/integration/autoland/rev/d1eb98680c78 Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug
Pushed by amarc@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/f88dc2eee3af https://hg.mozilla.org/integration/autoland/rev/f7c617595dfa Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing multiple failures

Backed out for causing multiple failures

Flags: needinfo?(krosylight)
Pushed by sstanca@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/edfb027f321f https://hg.mozilla.org/mozilla-central/rev/9b69ffb62ec6 Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing multiple failures
Status: ASSIGNED → RESOLVED
Closed: 2 months ago
Resolution: --- → FIXED
Target Milestone: --- → 157 Branch

Authored by https://github.com/saschanaz
https://github.com/mozilla/enterprise-firefox/commit/8f580a73d6b904bfcf94fbd7e087595d3bd2db27
[enterprise-main] Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug

Authored by https://github.com/amarc-mozilla
https://github.com/mozilla/enterprise-firefox/commit/73db385bb2d47d005dc340c54da0a5517bea8cf4
[enterprise-main] Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing lint failure @ shutdown.html

Authored by https://github.com/saschanaz
https://github.com/mozilla/enterprise-firefox/commit/d4bea9f805fc73867ff7ee7a344e86308569dc46
[enterprise-main] Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug

Authored by https://github.com/amarc-mozilla
https://github.com/mozilla/enterprise-firefox/commit/f88dc2eee3af6587d116aa5af8d5c5c095d144ab
[enterprise-main] Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing multiple failures

Authored by https://github.com/amarc-mozilla
https://github.com/mozilla/enterprise-firefox/commit/edfb027f321fd2584c8fd2df528e665b7f5b8dac
[enterprise-main] Revert "Bug 2065919 - Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug" for causing multiple failures

It's backed out?

Status: RESOLVED → REOPENED
Resolution: FIXED → ---

(In reply to Kagami Rosylight πŸ³οΈβ€πŸŒˆπŸ³οΈβ€βš§οΈ [:saschanaz] (they/them) from comment #2)

It seems this affects Chrome too. This may be a spec level issue.

One-line version repro:

data:text/html,<script>let rc, wc; const rs = new ReadableStream({ start(c) { rc = c; } }); const ws = new WritableStream({ start(c) { wc = c; }, write(chunk) {} }); rs.pipeTo(ws, { preventCancel: true }).catch(() => {}); setTimeout(() => { wc.error(new Error("boom")); rc.enqueue("chunk"); }, 100);</script>

Closing the loop - the spec is kinda handwavvy here, so it's unclear it's really spec issue.

Pushed by krosylight@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/c6f59b9c6d2b https://hg.mozilla.org/integration/autoland/rev/8ed296f5cd81 Check shutdown again before async write on read fulfill r=dom-core-reviewers,smaug
Status: REOPENED → RESOLVED
Closed: 2 months ago → 2 months ago
Resolution: --- → FIXED

Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/62554 for changes under testing/web-platform/tests

Whiteboard: [affects Chrome also][client-bounty-form] → [affects Chrome also][client-bounty-form], [wptsync upstream]

Upstream PR merged by moz-wptsync-bot

Do we need this on ESR153 also?

Flags: needinfo?(krosylight)
Flags: in-testsuite+
Attachment #9653517 - Flags: approval-mozilla-esr153?

firefox-esr153 Uplift Approval Request

  • User impact if declined/Reason for urgency: Can cause crash on tabs
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: only one extra check
  • String changes made/needed?: N/A
  • Is Android affected?: yes
Attachment #9653517 - Flags: approval-mozilla-esr153? → approval-mozilla-esr153+
QA Whiteboard: [qa-triage-done-c161/b160]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: