Closed Bug 2044626 Opened 4 months ago Closed 4 months ago

Pinned RTCEncodedFrame.data detach failure causes native WebRTC frame use-after-free

Categories

(Core :: WebRTC, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2043188

People

(Reporter: deayzl, Unassigned)

Details

(Keywords: csectype-uaf, reporter-external, sec-high, Whiteboard: [client-bounty-form])

Attachments

(4 files)

Summary

Firefox exposes RTCEncodedVideoFrame.data / RTCEncodedAudioFrame.data as a SpiderMonkey ArrayBuffer backed directly by libwebrtc-owned encoded-frame storage via JS::NewArrayBufferWithUserOwnedContents().
That storage must be detached before the native owner frees it, but RTCEncodedFrameBase::DetachData() ignores the fallible return value from JS::DetachArrayBuffer().

If web content pins a typed-array view of frame.data while worker GC collects the dropped RTCEncodedFrame, JS::DetachArrayBuffer() fails with RangeError: attempting to change pinned length of ArrayBuffer.
Firefox then still destroys the native libwebrtc frame, leaving the retained JS view attached to freed native EncodedImageBuffer memory.

The attached arbitrary-HTML repro uses WebRTC encoded transforms plus worker OPFS FileSystemSyncAccessHandle.write() as the length-pinning primitive and native stale-pointer read sink.

Reproduction Steps

tested firefox commit version: ef00d01b35bdd0f6bd1b444f78c693f246b7f58b

Build an ASAN firefox with:

mk_add_options MOZ_OBJDIR=/home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser

ac_add_options --enable-project=browser
ac_add_options --enable-address-sanitizer
ac_add_options --disable-install-strip
ac_add_options --disable-jemalloc
ac_add_options --disable-crashreporter
ac_add_options --disable-profiling

# Keep stack use reasonable while retaining useful line information.
ac_add_options --enable-optimize="-O1"
ac_add_options --enable-debug-symbols=-gline-tables-only

# Optional but useful for GC-sensitive SpiderMonkey reproduction work.
ac_add_options --enable-gczeal

Serve the attached html and js:

python3 -m http.server 8125 --bind 127.0.0.1

Execute:

rm -rf /tmp/ff-asan-webrtc-native-sink
mkdir -p /tmp/ff-asan-webrtc-native-sink2
ASAN_OPTIONS='detect_leaks=0:abort_on_error=1:handle_segv=2' \
  /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/bin/firefox \
  --no-remote \
  --profile /tmp/ff-asan-webrtc-native-sink2 \
  'http://127.0.0.1:8125/dual-opfs-native-sink.html'

Result:

=================================================================
==2116684==ERROR: AddressSanitizer: heap-use-after-free on address 0x7630dc6db800 at pc 0x58022c77e6d9 bp 0x7630d4d2aa10 sp 0x7630d4d2a1e0
READ of size 1048576 at 0x7630dc6db800 thread T40
    #0 0x58022c77e6d8 in write /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:1137:16
    #1 0x7a3129cdd3ed in pt_Write /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptio.c:1110:11
    #2 0x76310f25e396 in (anonymous namespace)::interposedWrite(PRFileDesc*, void const*, int) /home/slave/firefox-bug-bounty/firefox/xpcom/build/NSPRInterposer.cpp:102:10
    #3 0x76310dc014f9 in nsFileStreamBase::Write(char const*, unsigned int, unsigned int*) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/netwerk/base/./../../../firefox/netwerk/base/nsFileStreams.cpp:272:17
    #4 0x76311815b3f9 in Write /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/include/nsFileStreams.h:273:30
    ...
    #21 0x7a3129ce5be5 in _pt_root /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:190:3
    #22 0x58022c7fc6a6 in asan_thread_start(void*) /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:239:28
    #23 0x7a3129a9caa3 in start_thread nptl/pthread_create.c:447:8
    #24 0x7a3129b29c6b in clone3 misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:78

0x7630dc6db800 is located 0 bytes inside of 1048576-byte region [0x7630dc6db800,0x7630dc7db800)
freed by thread T29 here:
    #0 0x58022c8002b6 in free /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:51:3
    #1 0x7631176672db in operator delete[] /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/include/mozilla/cxxalloc.h:73:10
    #2 0x7631176672db in operator()<unsigned char> /home/slave/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/unique_ptr.h:120:4
    #3 0x7631176672db in ~unique_ptr /home/slave/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/unique_ptr.h:612:4
    #4 0x7631176672db in ~BufferT /home/slave/firefox-bug-bounty/firefox/third_party/libwebrtc/rtc_base/buffer.h:141:43
    #5 0x7631176672db in ~EncodedImageBuffer /home/slave/firefox-bug-bounty/firefox/third_party/libwebrtc/api/video/encoded_image.h:53:18
    ...
    #62 0x76311468d4c5 in bool mozilla::dom::binding_detail::GenericMethod<mozilla::dom::binding_detail::NormalThisPolicy, mozilla::dom::binding_detail::ThrowExceptions>(JSContext*, unsigned int, JS::Value*) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dom/bindings/./../../../firefox/dom/bindings/BindingUtils.cpp:3217:13

previously allocated by thread T35 here:
    #0 0x58022c800554 in malloc /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:67:3
    #1 0x58022c85345d in moz_xmalloc /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/memory/mozalloc/./../../../firefox/memory/mozalloc/mozalloc.cpp:48:15
    #2 0x763117663ac6 in operator new[] /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/include/mozilla/cxxalloc.h:42:10
    #3 0x763117663ac6 in BufferT /home/slave/firefox-bug-bounty/firefox/third_party/libwebrtc/rtc_base/buffer.h:408:31
    #4 0x763117663ac6 in CreateUninitializedWithSize /home/slave/firefox-bug-bounty/firefox/third_party/libwebrtc/rtc_base/buffer.h:77:12
    #5 0x763117663ac6 in EncodedImageBuffer /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/third_party/libwebrtc/api/video/encoded_image_gn/./../../../../../../firefox/third_party/libwebrtc/api/video/encoded_image.cc:27:15
    ...
    #42 0x58022c7fc6a6 in asan_thread_start(void*) /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:239:28

Thread T40 created by T0 (Isolated Web Co) here:
    #0 0x58022c7e4851 in pthread_create /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:250:3
    #1 0x7a3129cd5a00 in _PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:428:10
    #2 0x7a3129cc173e in PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:495:10
    #3 0x76310f1fe7fc in nsThread::Init(nsTSubstring<char> const&) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/xpcom/threads/./../../../firefox/xpcom/threads/nsThread.cpp:623:20
    #4 0x76310f20bf82 in nsThreadManager::NewNamedThread(nsTSubstring<char> const&, nsIThreadManager::ThreadCreationOptions, nsIThread**) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/xpcom/threads/./../../../firefox/xpcom/threads/nsThreadManager.cpp:618:22
    #5 0x76310f2185c7 in NS_NewNamedThread(nsTSubstring<char> const&, nsIThread**, already_AddRefed<nsIRunnable>, nsIThreadManager::ThreadCreationOptions) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/xpcom/threads/./../../../firefox/xpcom/threads/nsThreadUtils.cpp:186:57
    ...
    #38 0x7a3129a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #39 0x7a3129a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #40 0x58022c75d448 in _start (/home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/bin/firefox+0xb8448) (BuildId: d400de8eb99e19a70f3700a7436f261f)

Thread T29 created by T0 (Isolated Web Co) here:
    #0 0x58022c7e4851 in pthread_create /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:250:3
    #1 0x7a3129cd5a00 in _PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:428:10
    #2 0x7a3129cc173e in PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:495:10
    ...
    #52 0x7a3129a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #53 0x7a3129a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #54 0x58022c75d448 in _start (/home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/bin/firefox+0xb8448) (BuildId: d400de8eb99e19a70f3700a7436f261f)

Thread T35 created by T0 (Isolated Web Co) here:
    #0 0x58022c7e4851 in pthread_create /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:250:3
    #1 0x7a3129cd5a00 in _PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:428:10
    #2 0x7a3129cc173e in PR_CreateThread /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptthread.c:495:10
    #3 0x76310f1fe7fc in nsThread::Init(nsTSubstring<char> const&) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/xpcom/threads/./../../../firefox/xpcom/threads/nsThread.cpp:623:20
    #4 0x76310f20bf82 in nsThreadManager::NewNamedThread(nsTSubstring<char> const&, nsIThreadManager::ThreadCreationOptions, nsIThread**) /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/xpcom/threads/./../../../firefox/xpcom/threads/nsThreadManager.cpp:618:22
    ...
    #36 0x7a3129a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #37 0x7a3129a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #38 0x58022c75d448 in _start (/home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/dist/bin/firefox+0xb8448) (BuildId: d400de8eb99e19a70f3700a7436f261f)

SUMMARY: AddressSanitizer: heap-use-after-free /home/slave/firefox-bug-bounty/obj-x86_64-pc-linux-gnu-asan-browser/config/external/nspr/pr/./../../../../../firefox/nsprpub/pr/src/pthreads/ptio.c:1110:11 in pt_Write
Shadow bytes around the buggy address:
  0x7630dc6db580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7630dc6db800:[00]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6db980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6dba00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7630dc6dba80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==2116684==ABORTING
Flags: sec-bounty?
Attached file asan-uaf.log —

I'll add root cause analysis on this bug later.

Group: firefox-core-security → media-core-security
Component: Security → WebRTC
Product: Firefox → Core

Please don't put the URL of the Firefox GitHub repository in the URL field. It is not helpful.

Root Cause Analysis

analyzed firefox commit version: fc12c6f52aea47b9ca635b0ab71315ffbeb8c2f9

The receiver WebRTC transform worker receives an RTCEncodedVideoFrame:

(When transforming a frame, it is wrapped as a RTCEncodedVideoFrame)

// dom/media/webrtc/jsapi/RTCRtpScriptTransformer.cpp
void RTCRtpScriptTransformer::TransformFrame(
    std::unique_ptr<webrtc::TransformableFrameInterface> aFrame) {
  ...

  RefPtr<RTCEncodedFrameBase> domFrame;
  if (*mVideo) {
    ...
    domFrame = new RTCEncodedVideoFrame(mGlobal, std::move(aFrame),
                                        ++mLastEnqueuedFrameCounter, this,
                                        mTimestampMaker);
  } else {
    domFrame = new RTCEncodedAudioFrame(mGlobal, std::move(aFrame),
                                        ++mLastEnqueuedFrameCounter, this,
                                        mTimestampMaker);
  }
  mReadableSource->Enqueue(domFrame);
}

(RTCEncodedFrameBase creates a JS ArrayBuffer pointing at native WebRTC payload memory)

// dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
RTCEncodedFrameBase::RTCEncodedFrameBase(nsIGlobalObject* aGlobal,
                                         RTCEncodedFrameState& aState,
                                         RTCRtpScriptTransformer* aOwner)
    : mGlobal(aGlobal), mOwner(aOwner), mState(aState), mData(nullptr) {
  ...

  if (mState.mFrame->GetData().data()) {
    // Avoid a copy
    mData = JS::NewArrayBufferWithUserOwnedContents(
        jsapi.cx(), mState.mFrame->GetData().size(),
        (void*)(mState.mFrame->GetData().data()));
  } else {
    mData = JS::NewArrayBuffer(jsapi.cx(), 0);
  }
}

The worker of receiver saves new Uint8Array(frame.data) and drops JS references to the frame wrapper:

(new Uint8Array(frame.data) retains the ArrayBuffer)

onrtctransform = event => {
      ...
      let result = await reader.read();
      ...

      let frame = result.value;
      const id = ++received;
      const view = new Uint8Array(frame.data);
      result.value = null;
      result = null;
      frame = null;
      retained.push(view)

The worker of receiver queues allocation pressure to make GC happen, then calls FileSystemSyncAccessHandle.write(view). ProcessTypedArraysFixed() pins the view length, and OPFS enters syncLoop.Run() before the pin is released:

// dom/fs/api/FileSystemSyncAccessHandle.cpp
uint64_t FileSystemSyncAccessHandle::ReadOrWrite(
    const AllowSharedBufferSource& aBuffer,
    const FileSystemReadWriteOptions& aOptions, const bool aRead,
    ErrorResult& aRv) {
  ...

  AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling);

  nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
      syncLoop.GetSerialEventTarget();
  QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aRv](nsresult) {
    aRv.ThrowInvalidStateError("Worker is shutting down");
    return 0;
  });

  uint64_t totalCount = 0;

  ProcessTypedArraysFixed(aBuffer, [&](const Span<uint8_t> aData) {
    InvokeAsync(
        mIOTaskQueue, __func__,
        [selfHolder = quota::TargetPtrHolder(this), aData,
         use_offset = aOptions.mAt.WasPassed(), offset, aRead, syncLoopTarget,
         &totalCount]() {
          ...
        })
        ->Then(syncLoopTarget, __func__,
               [this, &syncLoopTarget](
                   const BoolPromise::ResolveOrRejectValue& aValue) {
                 MOZ_ASSERT(mWorkerRef);

                 mWorkerRef->Private()->AssertIsOnWorkerThread();

                 mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK);
               });

    MOZ_ALWAYS_SUCCEEDS(syncLoop.Run());
  });

  return totalCount;
}
// dom/bindings/TypedArray.h
template <typename T, typename Processor,
          std::enable_if_t<is_dom_union_with_typedarray_members<T>, int> = 0>
[[nodiscard]] auto ProcessTypedArraysFixed(const T& aUnion,
                                           Processor&& aProcessor) {
  return binding_detail::ApplyToTypedArrays(
      aUnion, [&](const auto& aTypedArray) {
        return aTypedArray.ProcessFixedData(
            std::forward<Processor>(aProcessor));
      });
}

  template <bool AllowLargeTypedArrays = false, typename Processor>
  [[nodiscard]] ProcessReturnType<Processor> ProcessFixedData(
      Processor&& aProcessor) const {
    ...
    LengthPinner pinner(this); // pin length

    return CallProcessor(GetCurrentData<AllowLargeTypedArrays>(),
                         std::forward<Processor>(aProcessor));
  }

The worker sync loop processes events and GC/CC collects the dropped RTCEncodedVideoFrame then RTCEncodedFrameBase::~RTCEncodedFrameBase() calls DetachData():

// dom/workers/WorkerPrivate.cpp
nsresult WorkerPrivate::RunCurrentSyncLoop() {
    ...

      if (normalRunnablesPending) {
        ...
        if (GetCurrentEventLoopGlobal()) {
          ...
          JS_MaybeGC(cx);
        }
      }
    }
  }
  ...
}
// dom/workers/RuntimeService.cpp
class WorkerJSRuntime final : public mozilla::CycleCollectedJSRuntime {
  ...
  virtual void CustomGCCallback(JSGCStatus aStatus) override {
    ...

    if (aStatus == JSGC_END) {
      bool collectedAnything =
          nsCycleCollector_collect(CCReason::GC_FINISHED, nullptr); // collect `RTCEncodedVideoFrame`
      mWorkerPrivate->SetCCCollectedAnything(collectedAnything);
    }
  }
  ...
// dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
RTCEncodedFrameBase::~RTCEncodedFrameBase() {
  DetachData();
  mData = nullptr;
  mozilla::DropJSObjects(this);
}

void RTCEncodedFrameBase::DetachData() {
  // We might have handled this in unlink already
  if (mGlobal && mData) {
    AutoJSAPI jsapi;
    if (NS_WARN_IF(!jsapi.Init(mGlobal))) {
      return;
    }

    JS::Rooted<JSObject*> rootedData(jsapi.cx(), mData);
    if (rootedData) {
      JS::DetachArrayBuffer(jsapi.cx(), rootedData);
    } // ignore failure of `JS::DetachArrayBuffer`
  }
}

JS::DetachArrayBuffer() fails because the retained view is length-pinned, and reports RangeError: attempting to change pinned length of ArrayBuffer:

// js/src/vm/ArrayBufferObject.cpp
JS_PUBLIC_API bool JS::DetachArrayBuffer(JSContext* cx, HandleObject obj) {
  ...
  if (unwrappedBuffer->isLengthPinned()) { // true
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_ARRAYBUFFER_LENGTH_PINNED);
    return false;
  }

  AutoRealm ar(cx, unwrappedBuffer);
  ArrayBufferObject::detach(cx, unwrappedBuffer);
  return true;
}
// js/public/friend/ErrorNumbers.msg
MSG_DEF(JSMSG_ARRAYBUFFER_LENGTH_PINNED, 0, JSEXN_RANGEERR, "attempting to change pinned length of ArrayBuffer")

RTCEncodedFrameBase::DetachData() ignores the failed detach and teardown continues.

RTCEncodedFrameState::~RTCEncodedFrameState() destroys the native WebRTC frame, freeing the libwebrtc EncodedImageBuffer:

// dom/media/webrtc/jsapi/RTCEncodedFrameBase.h
struct RTCEncodedFrameState {
  std::unique_ptr<webrtc::TransformableFrameInterface> mFrame;
  uint64_t mCounter = 0;
  unsigned long mTimestamp = 0;
  ...
// dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
RTCEncodedFrameState::~RTCEncodedFrameState() = default;
// third_party/libwebrtc/modules/rtp_rtcp/source/frame_object.h
class RtpFrameObject : public EncodedFrame {
  ...
 private:
  // Reference for mutable access.
  scoped_refptr<EncodedImageBuffer> image_buffer_;
  ...

The retained JS view still has byteLength == 1048576 and still points at the freed native payload.

The repro passes that retained view into OPFS; the native file write path reads 1 MiB from the stale pointer and ASAN reports heap-use-after-free:

(OPFS creates NS_NewByteInputStream(..., AsChars(aData), NS_ASSIGNMENT_DEPEND) and NS_ASSIGNMENT_DEPEND refers directly to the caller’s buffer)

// dom/fs/api/FileSystemSyncAccessHandle.cpp
uint64_t FileSystemSyncAccessHandle::ReadOrWrite(
    const AllowSharedBufferSource& aBuffer,
    const FileSystemReadWriteOptions& aOptions, const bool aRead,
    ErrorResult& aRv) {
      ...
            QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream(
                       getter_AddRefs(inputStream), AsChars(aData),
                       NS_ASSIGNMENT_DEPEND)),
                   CreateAndRejectBoolPromise);
// xpcom/io/nsStringStream.cpp
NS_IMETHODIMP
nsStringInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
                                  uint32_t aCount, uint32_t* aResult) {
  ...
  nsresult rv = aWriter(this, aClosure, source->Data().Elements() + offset, 0,
                        aCount, aResult);

...

// xpcom/build/NSPRInterposer.cpp
int32_t PR_CALLBACK interposedWrite(PRFileDesc* aFd, const void* aBuf,
                                    int32_t aAmt) {
  // If we don't have a valid original function pointer something is very wrong.
  NS_ASSERTION(sWriteFn, "NSPR IO Interposing: sWriteFn is NULL");

  NSPRIOAutoObservation timer(mozilla::IOInterposeObserver::OpWrite, aFd);
  return sWriteFn(aFd, aBuf, aAmt); // heap-use-after-free
}

Here's precise summary:

first write(view):
    pins ArrayBuffer length
    enters worker sync loop
        GC/CC runs
        DetachArrayBuffer fails because pinned
        native frame is freed anyway
    view survives with stale pointer

later native use of view:
    reads freed native frame memory -> UAF

any update on this bug?

Status: UNCONFIRMED → RESOLVED
Closed: 4 months ago
Duplicate of bug: CVE-2026-16362
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
Group: media-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: