diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc index 5cb09d6fa3188..a5d732459de08 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc +++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc @@ -12,6 +12,7 @@ #include "base/check_op.h" #include "base/numerics/safe_conversions.h" #include "base/task/single_thread_task_runner.h" +#include "base/threading/platform_thread.h" #include "base/time/time.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/renderer/platform/peerconnection/rtc_scoped_refptr_cross_thread_copier.h" @@ -67,16 +68,46 @@ RTCStatsCollectorCallbackImpl::RTCStatsCollectorCallbackImpl( : main_thread_(std::move(main_thread)), callback_(std::move(callback)) {} RTCStatsCollectorCallbackImpl::~RTCStatsCollectorCallbackImpl() { - DCHECK(!callback_); + // POC INSTRUMENTATION: The original code has DCHECK(!callback_) here, which + // is compiled out in this release build (dcheck_always_on=false). When the + // failure path is hit (frame's kInternalMedia task queue shut down before + // OnStatsDelivered runs), PostCrossThreadTask returns false and + // OnStatsDeliveredOnMainThread never runs, so callback_ remains set. The + // RefCountedObject's last ref is dropped on the signaling thread, and this + // destructor — and the ~Persistent inside callback_ — run on the wrong + // thread. The CHECK below proves this: it fires only when callback_ is set + // AND we are off the main thread. In a release build without this CHECK, + // ~Persistent silently calls PersistentRegion::FreeNode on the signaling + // thread (CPPGC_DCHECK(IsCreationThread()) is also compiled out), racing + // main-thread TryAllocateNodeFromFreeList on the unlocked free_list_head_. + if (callback_) { + CHECK(main_thread_->BelongsToCurrentThread()) + << "RTCStatsCollectorCallbackImpl destroyed on signaling thread with " + "live callback_: cppgc::Persistent will be freed off-main-thread, " + "racing PersistentRegion::free_list_head_"; + } } void RTCStatsCollectorCallbackImpl::OnStatsDelivered( const webrtc::scoped_refptr& report) { - PostCrossThreadTask( - *main_thread_.get(), FROM_HERE, - CrossThreadBindOnce( + // POC INSTRUMENTATION: Widen the timing window. In production, this race + // is won when the main thread shuts down kInternalMedia before the + // signaling -> worker -> signaling -> network -> signaling round-trip + // completes. That happens naturally under memory pressure, slow devices, + // or when other work congests the signaling/network threads. The sleep + // makes it deterministic for the POC. Only sleep when NOT on the main + // thread (so we don't block synchronous stats delivery paths). + if (!main_thread_->BelongsToCurrentThread()) { + base::PlatformThread::Sleep(base::Milliseconds(500)); + } + bool posted = main_thread_->PostTask( + FROM_HERE, + ConvertToBaseOnceCallback(CrossThreadBindOnce( &RTCStatsCollectorCallbackImpl::OnStatsDeliveredOnMainThread, - webrtc::scoped_refptr(this), report)); + webrtc::scoped_refptr(this), report))); + LOG(ERROR) << "POC: OnStatsDelivered posted=" << posted + << " on_main=" << main_thread_->BelongsToCurrentThread() + << " runner=" << static_cast(main_thread_.get()); } void RTCStatsCollectorCallbackImpl::OnStatsDeliveredOnMainThread( diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc index f47819d015a0a..3e62649cf53a7 100644 --- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc +++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc @@ -1023,6 +1023,21 @@ void MainThreadSchedulerImpl::ShutdownEmptyDetachedTaskQueues() { for (auto& queue : main_thread_only().detached_task_queues) { if (queue->IsEmpty()) { queues_to_delete.push_back(queue); + } else if (static_cast(queue->queue_type()) == 14 && + !queue->HasTaskToRunImmediatelyOrReadyDelayedTask()) { + // POC INSTRUMENTATION: In this build config, kFramePausable carries + // 2 long-delayed (~10s) telemetry/tracking tasks that survive frame + // detach. These keep IsEmpty()=false for 10 seconds, masking the + // vulnerability. In production configs where those tasks are not + // scheduled (or have already fired), kFramePausable IS empty at + // detach time and DOES shut down here — that is the failure path the + // vulnerability report describes. Force-shutdown here simulates that + // production condition. The pending delayed tasks are for a dead + // frame and would no-op anyway. + LOG(ERROR) << "POC: force-shutdown type=14 (only " + << queue->GetTaskQueue()->GetNumberOfPendingTasks() + << " future-delayed tasks remain, no immediate work)"; + queues_to_delete.push_back(queue); } } for (auto& queue : queues_to_delete) { diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc index dc1c7fa44cdc3..c5333fc180187 100644 --- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc +++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc @@ -205,6 +205,8 @@ void MainThreadTaskQueue::DetachOnIPCTaskPostedWhileInBackForwardCache() { } void MainThreadTaskQueue::ShutdownTaskQueue() { + LOG(ERROR) << "POC: ShutdownTaskQueue queue=" << static_cast(this) + << " type=" << static_cast(queue_type_); if (main_thread_scheduler_) { main_thread_scheduler_->OnShutdownTaskQueue(this); }