Chrome · GPU › GPU Security
CVE-2026-95322
OOB in GPU (GPU Security)
Critical
watch
CVSS 8.3
chrome
OOB
🔧 Commit mapped
🧬 Trusting GPU driver quirks, clear state, or layout math
Overview
Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Background
- `AHardwareBuffer`
- Android’s native graphics buffer; arrives over Mojo as a socket endpoint reconstructed by the platform, so everything
AHardwareBuffer_describe()reports (width,height,layers,usage, strides) is attacker-controlled when it originates from a compromised renderer. - NV12
- a YUV 4:2:0 format with a full-resolution luma (Y) plane followed by an interleaved half-resolution chroma (UV) plane; its size is
width*height(Y) plus2 * ((width+1)/2) * ((height+1)/2)(UV). - `libyuv::NV12Copy()`
- the copy routine used for readback; it rounds chroma dimensions up via
halfwidth = (width+1)>>1andhalfheight = (height+1)>>1. - `GpuMemoryBufferHandle`
- the readback destination descriptor passed from the renderer to the GPU process; the shared-memory region sized from it must be large enough to hold the copied planes.
Root Cause Analysis
CopyNativeGmbToSharedMemoryAsync() / CopyNativeBufferToSharedMemoryAsync() sized the destination shared-memory region with the integer expression width * height * 3 / 2, which rounds the chroma contribution down, while the subsequent libyuv::NV12Copy() computes chroma extents by rounding up. For a forged buffer with an odd width such as 2730667x1, the size check accepts a 4096000-byte region but the copy writes 5461335 bytes, a 1365335-byte out-of-bounds write filled with renderer-controlled pixel data. The odd-dimension rejection in IsSizeForBufferHandleValid() normally prevents this, but it was bypassed here because the dimensions were read from the untrusted AHardwareBuffer description rather than validated over the Mojo wire. Additional attack surface came from a CHECK() on desc.usage, from unchecked narrowing of 64-bit-capable dimensions to int, and from unvalidated source row strides. None of these untrusted values were range-checked before being used to size allocations or drive the copy.Key insight
The size check and the copy used different rounding conventions for chroma dimensions (
/2 truncates down vs. (x+1)>>1 rounds up), so an odd dimension sourced from the attacker-controlled buffer description makes the validated region smaller than what libyuv actually writes.Attack Path
- Compromise the renderer An attacker gains code execution in a renderer process able to issue GPU-service readback commands.
- Forge the buffer description
The renderer sends a
GpuMemoryBufferHandlewhose reconstructedAHardwareBufferreports odd, oversized dimensions (e.g.2730667x1) that never pass throughIsSizeForBufferHandleValid(). - Trigger NV12 readback
The renderer invokes
CopyNativeGmbToSharedMemoryAsync()/CopyNativeBufferToSharedMemoryAsync(), which sizes the destination with the truncatingwidth * height * 3 / 2expression. - Overflow the destination
libyuv::NV12Copy()rounds chroma up and writes past the undersized shared-memory region, producing a~1.3 MBOOB write with attacker-controlled contents in the GPU process.
Impact Assessment
This is a renderer-triggerable out-of-bounds write of over a megabyte of attacker-controlled data in the GPU process, corrupting adjacent heap memory. Combined with the crash primitive from the removed
CHECK() on attacker-controlled usage bits, it gives an attacker both a denial-of-service and a strong foundation for GPU-process memory corruption and privilege escalation out of the renderer sandbox.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc |
modified | |
ifgpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc |
modified | |
forgpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc |
modified | |
TESTgpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.ccgpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc
Audit Directions
- Mismatched rounding between validation and useWherever a size check divides by 2 (or truncates) for chroma/subsampled planes while the actual copy uses
(x+1)>>1rounding (e.g.libyuvNV12/I420 helpers), the validated region can be smaller than what is written; require both paths to use the same plane-size helper such asviz::SharedMemoryPlaneSizeForSharedImageFormat(). - Trusting reconstructed native handlesValues from
AHardwareBuffer_describe()and similar platform-reconstructed handles (width,height,layers,usage, strides) are attacker-controlled; validate them withIsSizeForBufferHandleValid()and range-check before narrowing tointrather than assuming wire-level validation applied. - `CHECK()` on attacker-controlled inputAudit for
CHECK()/DCHECK()guarding renderer-supplied fields (e.g.desc.usage); these turn malformed input into GPU-process crashes and should be replaced with graceful failure.
Patch
From 1ef712bafebc527732f7e769f168bafb8d1022e9 Mon Sep 17 00:00:00 2001 From: vikas soni <[email protected]> Date: Tue, 15 Sep 2026 15:21:01 -0700 Subject: [PATCH] [GPU Security]: Validate untrusted AHardwareBuffer description before NV12 readback. CopyNativeGmbToSharedMemoryAsync() accepts a GpuMemoryBufferHandle from a potentially compromised renderer. An AHardwareBuffer travels over Mojo as a Unix socket endpoint that the platform reconstructs the buffer from, so neither Mojo nor the GPU process can authenticate who allocated it. Everything AHardwareBuffer_describe() reports is therefore attacker controlled input. CopyNativeBufferToSharedMemoryAsync() sized the destination with `width * height * 3 / 2`, which rounds the chroma planes down, and then copied with libyuv::NV12Copy(), which rounds them up (halfwidth = (width + 1) >> 1). For a forged 2730667x1 buffer the check accepts a 4096000 byte region while the copy writes 5461335 bytes, an out of bounds write of 1365335 bytes whose contents come from the renderer supplied source buffer. Odd dimensions are rejected everywhere else on Android by IsSizeForBufferHandleValid(), but that check is bypassed here because the size is derived from the untrusted buffer rather than sent over the wire. Treat the buffer description as untrusted input: * Reject odd dimensions with IsSizeForBufferHandleValid(), the same predicate used when creating such SharedImages. This removes the floor/ceil divergence and makes the "UV row size == width" assumption of the destination layout true. * Size the destination with viz::SharedMemoryPlaneSizeForSharedImageFormat(), the helper the client uses to allocate the region, and sum the planes with base::CheckedNumeric. * Hand libyuv bounds checked spans obtained with span::split_at() instead of a subspan() offset computed in 32 bit arithmetic. * Range check the dimensions before narrowing them to int, reject empty sizes, and reject buffers with layers != 1. * Turn the CHECK() on desc.usage into a graceful failure. The usage bits are attacker controlled, so the CHECK() let a renderer crash the GPU process at will. * Sanity check the source row strides. Add regression tests covering the odd dimension overflow, the destination size boundary, and non CPU readable buffers. Bug: 556576992 Change-Id: I90474d22a5e1ea12235fa0ac419ff5c0c13c36ef Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8395051 Commit-Queue: vikas soni <[email protected]> Reviewed-by: Vasiliy Telezhnikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698422} --- diff --git a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc index 2b80f4040..b97b6ef6 100644 --- a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc +++ b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc @@ -10,6 +10,7 @@ #include <algorithm> #include <memory> +#include <optional> #include <utility> #include <vector> @@ -23,10 +24,12 @@ #include "base/memory/raw_ptr.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_refptr.h" +#include "base/numerics/safe_conversions.h" #include "base/posix/eintr_wrapper.h" #include "base/strings/string_number_conversions.h" #include "base/trace_event/trace_event.h" #include "build/build_config.h" +#include "components/viz/common/resources/shared_image_format_utils.h" #include "gpu/command_buffer/common/gles2_cmd_utils.h" #include "gpu/command_buffer/common/shared_image_usage.h" #include "gpu/command_buffer/service/ahardwarebuffer_utils.h" @@ -1179,23 +1182,60 @@ CHECK(desc.usage & (AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN)); - base::span<uint8_t> dst_buffer = mapping.GetMemoryAsSpan<uint8_t>(); - const size_t required_size = - static_cast<size_t>(desc.width) * desc.height * 3 / 2; - if (dst_buffer.size() < required_size) { + // gfx::Size and libyuv take the dimensions as `int`. + if (!base::IsValueInRangeForNumericType<int>(desc.width) || + !base::IsValueInRangeForNumericType<int>(desc.height)) { + return false; + } + const gfx::Size size(static_cast<int>(desc.width), + static_cast<int>(desc.height)); + if (size.IsEmpty()) { return false; } + // Clients can't create a non-NV12 mappable AHB SharedImage. + // NV12 requires even dimensions on this platform, and the code below relies + // on that: it writes the destination UV rows with a stride of `width` bytes, + // while libyuv::NV12Copy() writes ceil(width / 2) * 2 bytes per UV row over + // ceil(height / 2) rows. For odd dimensions the two disagree, which is what + // allowed a forged buffer to overflow the destination. + constexpr viz::SharedImageFormat kFormat = viz::MultiPlaneFormat::kNV12; + if (!IsSizeForBufferHandleValid(size, kFormat)) { + return false; + } + + // Size the destination with the same helpers the client used to allocate + // `shared_memory`. They use ceil-rounded chroma dimensions, matching what + // NV12Copy() writes, and checked arithmetic. + std::optional<size_t> y_plane_size = + viz::SharedMemoryPlaneSizeForSharedImageFormat(kFormat, /*plane_index=*/0, + size); + std::optional<size_t> required_size = + viz::SharedMemorySizeForSharedImageFormat(kFormat, size); + if (!y_plane_size.has_value() || !required_size.has_value()) { + return false; + } + + base::span<uint8_t> dst_buffer = mapping.GetMemoryAsSpan<uint8_t>(); + if (dst_buffer.size() < required_size.value()) { + return false; + } + // Bounds-checked split of the destination into its Y and UV planes. + auto [dst_y_plane, dst_uv_plane] = + dst_buffer.first(required_size.value()).split_at(y_plane_size.value()); + AHardwareBuffer_Planes planes; int fence = -1; - int ret = AHardwareBuffer_lockPlanes(hardware_buffer, - AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, - fence, nullptr, &planes); + const uint64_t cpu_read_usage = + desc.usage & (AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | + AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN); + int ret = AHardwareBuffer_lockPlanes(hardware_buffer, cpu_read_usage, fence, + nullptr, &planes); if (ret != 0) { return false; } - absl::Cleanup uncloker = [&]() { + absl::Cleanup unlocker = [&]() { AHardwareBuffer_unlock(hardware_buffer, nullptr); }; @@ -1219,14 +1259,35 @@ return false; } - const int dst_stride = desc.width; + // libyuv takes strides as `int`. Ensure the source strides fit in `int` to + // prevent overflow into negative values, which libyuv interprets as bottom-up + // strides. + if (!base::IsValueInRangeForNumericType<int>(planes.planes[0].rowStride) || + !base::IsValueInRangeForNumericType<int>(planes.planes[1].rowStride)) { + return false; + } + + // Destination row strides must match what the client expects when mapping + // the shared memory, and must fit in `int` for libyuv. + std::optional<size_t> dst_stride_y = + viz::SharedMemoryRowSizeForSharedImageFormat(kFormat, /*plane_index=*/0, + size.width()); + std::optional<size_t> dst_stride_uv = + viz::SharedMemoryRowSizeForSharedImageFormat(kFormat, /*plane_index=*/1, + size.width()); + if (!dst_stride_y.has_value() || !dst_stride_uv.has_value() || + !base::IsValueInRangeForNumericType<int>(dst_stride_y.value()) || + !base::IsValueInRangeForNumericType<int>(dst_stride_uv.value())) { + return false; + } int result = libyuv::NV12Copy( - static_cast<uint8_t*>(planes.planes[0].data), planes.planes[0].rowStride, - static_cast<uint8_t*>(planes.planes[1].data), planes.planes[1].rowStride, - dst_buffer.data(), dst_stride, - dst_buffer.subspan(desc.height * dst_stride).data(), dst_stride, - desc.width, desc.height); + static_cast<uint8_t*>(planes.planes[0].data), + static_cast<int>(planes.planes[0].rowStride), + static_cast<uint8_t*>(planes.planes[1].data), + static_cast<int>(planes.planes[1].rowStride), dst_y_plane.data(), + static_cast<int>(dst_stride_y.value()), dst_uv_plane.data(), + static_cast<int>(dst_stride_uv.value()), size.width(), size.height()); return result == 0; } diff --git a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc index f6733840..4fa7c8e 100644 --- a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc +++ b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc @@ -6,10 +6,16 @@ #include <android/hardware_buffer.h> +#include <vector> + #include "base/android/scoped_hardware_buffer_fence_sync.h" +#include "base/android/scoped_hardware_buffer_handle.h" #include "base/compiler_specific.h" +#include "base/containers/span.h" #include "base/functional/callback_helpers.h" #include "base/memory/raw_ptr.h" +#include "base/memory/unsafe_shared_memory_region.h" +#include "base/numerics/safe_conversions.h" #include "components/viz/common/resources/shared_image_format_utils.h" #include "gpu/command_buffer/common/mailbox.h" #include "gpu/command_buffer/common/shared_image_info.h" @@ -784,5 +790,172 @@ GrContextType::kGraphiteDawn), testing::PrintToStringParamName()); +// Usage bits of a real NV12 buffer that is read back to shared memory: it is +// sampled by the GPU and read by the CPU. Allocating with GPU usage matters, +// since a CPU-only Y8Cb8Cr8_420 allocation is free to come back as a planar +// (YV12) layout, which this code path does not support. +constexpr uint64_t kNV12ReadbackUsage = + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN; + +// Allocates an NV12 AHardwareBuffer of `size`, or returns an empty handle if +// the platform refuses the allocation (e.g. for odd dimensions). +base::android::ScopedHardwareBufferHandle AllocateNV12Buffer( + const gfx::Size& size, + uint64_t usage = kNV12ReadbackUsage) { + AHardwareBuffer_Desc desc = { + .width = static_cast<uint32_t>(size.width()), + .height = static_cast<uint32_t>(size.height()), + .layers = 1, + .format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420, + .usage = usage, + }; + AHardwareBuffer* buffer = nullptr; + AHardwareBuffer_allocate(&desc, &buffer); + if (!buffer) { + return base::android::ScopedHardwareBufferHandle(); + } + return base::android::ScopedHardwareBufferHandle::Adopt(buffer); +} + +// Whether the platform allocator hands back the interleaved NV12 plane layout +// that CopyNativeBufferToSharedMemoryAsync() supports. Gralloc is free to pick +// a planar layout instead, in which case the copy legitimately fails and the +// tests below that expect success have nothing to assert. +bool SupportsNV12PlaneLayout(AHardwareBuffer* buffer) { + AHardwareBuffer_Planes planes; + if (AHardwareBuffer_lockPlanes(buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, + /*fence=*/-1, nullptr, &planes) != 0) { + return false; + } + const bool supported = + planes.planeCount == 3 && planes.planes[0].pixelStride == 1 && + planes.planes[1].pixelStride == 2 && planes.planes[2].pixelStride == 2 && + planes.planes[1].rowStride == planes.planes[2].rowStride && + (static_cast<uint8_t*>(planes.planes[2].data) - + static_cast<uint8_t*>(planes.planes[1].data)) == 1; + AHardwareBuffer_unlock(buffer, nullptr); + return supported; +} + +// Writes a distinct value into every byte the copy is supposed to read, and +// returns those bytes in the order the destination is supposed to hold them: +// `height` rows of `width` Y bytes, followed by `height / 2` rows of `width` +// interleaved UV bytes. Returns an empty vector if the buffer can't be locked. +std::vector<uint8_t> FillNV12Buffer(AHardwareBuffer* buffer, + const gfx::Size& size) { + AHardwareBuffer_Planes planes; + if (AHardwareBuffer_lockPlanes(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, + /*fence=*/-1, nullptr, &planes) != 0) { + return {}; + } + + const size_t width = base::checked_cast<size_t>(size.width()); + const size_t height = base::checked_cast<size_t>(size.height()); + std::vector<uint8_t> expected; + auto fill_plane = [&](const AHardwareBuffer_Plane& src, size_t rows, + uint8_t seed) { + const size_t row_stride = src.rowStride; + // SAFETY: A locked plane holds `rows` rows at `rowStride` byte intervals, + // the last of which is at least `width` bytes long. + base::span<uint8_t> plane = UNSAFE_BUFFERS(base::span( + static_cast<uint8_t*>(src.data), (rows - 1) * row_stride + width)); + for (size_t row = 0; row < rows; ++row) { + base::span<uint8_t> src_row = plane.subspan(row * row_stride, width); + for (size_t col = 0; col < width; ++col) { + src_row[col] = static_cast<uint8_t>(seed + row * 7 + col); + expected.push_back(src_row[col]); + } + } + }; + fill_plane(planes.planes[0], height, /*seed=*/0); + fill_plane(planes.planes[1], height / 2, /*seed=*/128); + + AHardwareBuffer_unlock(buffer, nullptr); + return expected; +} + +// Regression test for the out-of-bounds write reachable from a compromised +// renderer via the CopyNativeGmbToSharedMemoryAsync IPC. The destination size +// used to be computed as `width * height * 3 / 2`, which rounds *down* for odd +// dimensions, while libyuv::NV12Copy() writes ceil-rounded chroma planes. A +// buffer with odd dimensions must be rejected outright. +TEST(AHardwareBufferImageBackingFactoryCopyTest, RejectsOddDimensions) { + constexpr gfx::Size kOddSize(65, 65); + auto ahb_handle = AllocateNV12Buffer(kOddSize); + if (!ahb_handle.is_valid()) { + GTEST_SKIP() << "Odd-sized NV12 AHardwareBuffer allocation unsupported"; + } + + // This is the size that the old (truncating) check would have accepted; it is + // smaller than what NV12Copy() would write, so accepting it overflows. + const size_t undersized = + static_cast<size_t>(kOddSize.width()) * kOddSize.height() * 3 / 2; + ASSERT_LT(undersized, viz::SharedMemorySizeForSharedImageFormat( + viz::MultiPlaneFormat::kNV12, kOddSize) + .value()); + auto region = base::UnsafeSharedMemoryRegion::Create(undersized); + ASSERT_TRUE(region.IsValid()); + + EXPECT_FALSE( + AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync( + gfx::GpuMemoryBufferHandle(std::move(ahb_handle)), + std::move(region))); +} + +// A destination smaller than the full NV12 image must be rejected. +TEST(AHardwareBufferImageBackingFactoryCopyTest, RejectsUndersizedDestination) { + constexpr gfx::Size kSize(64, 64); + auto ahb_handle = AllocateNV12Buffer(kSize); + if (!ahb_handle.is_valid()) { + GTEST_SKIP() << "NV12 AHardwareBuffer allocation unsupported"; + } + const size_t required = viz::SharedMemorySizeForSharedImageFormat( + viz::MultiPlaneFormat::kNV12, kSize) + .value(); + auto region = base::UnsafeSharedMemoryRegion::Create(required - 1); + ASSERT_TRUE(region.IsValid()); + + EXPECT_FALSE( + AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync( + gfx::GpuMemoryBufferHandle(std::move(ahb_handle)), + std::move(region))); +} + +// A correctly sized destination must receive the whole image in the layout the +// client expects: a tightly packed Y plane followed by a tightly packed UV +// plane, both with a row size of `width` bytes. +TEST(AHardwareBufferImageBackingFactoryCopyTest, CopiesBothPlanes) { + constexpr gfx::Size kSize(64, 64); + auto ahb_handle = AllocateNV12Buffer( + kSize, kNV12ReadbackUsage | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN); + if (!ahb_handle.is_valid()) { + GTEST_SKIP() << "NV12 AHardwareBuffer allocation unsupported"; + } + if (!SupportsNV12PlaneLayout(ahb_handle.get())) { + GTEST_SKIP() << "Platform does not provide an interleaved NV12 layout"; + } + + const std::vector<uint8_t> expected = FillNV12Buffer(ahb_handle.get(), kSize); + ASSERT_EQ(expected.size(), viz::SharedMemorySizeForSharedImageFormat( + viz::MultiPlaneFormat::kNV12, kSize) + .value()); + + auto region = base::UnsafeSharedMemoryRegion::Create(expected.size()); + ASSERT_TRUE(region.IsValid()); + base::UnsafeSharedMemoryRegion region_for_reading = region.Duplicate(); + ASSERT_TRUE(region_for_reading.IsValid()); + + ASSERT_TRUE( + AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync( + gfx::GpuMemoryBufferHandle(std::move(ahb_handle)), + std::move(region))); + + base::WritableSharedMemoryMapping mapping = region_for_reading.Map(); + ASSERT_TRUE(mapping.IsValid()); + EXPECT_EQ(mapping.GetMemoryAsSpan<uint8_t>().first(expected.size()), + base::span(expected)); +} + } // anonymous namespace } // namespace gpu
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc
index f6733840..4fa7c8e 100644
--- a/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory_unittest.cc
@@ -6,10 +6,16 @@
#include <android/hardware_buffer.h>
+#include <vector>
+
#include "base/android/scoped_hardware_buffer_fence_sync.h"
+#include "base/android/scoped_hardware_buffer_handle.h"
#include "base/compiler_specific.h"
+#include "base/containers/span.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
+#include "base/memory/unsafe_shared_memory_region.h"
+#include "base/numerics/safe_conversions.h"
#include "components/viz/common/resources/shared_image_format_utils.h"
#include "gpu/command_buffer/common/mailbox.h"
#include "gpu/command_buffer/common/shared_image_info.h"
@@ -784,5 +790,172 @@
GrContextType::kGraphiteDawn),
testing::PrintToStringParamName());
+// Usage bits of a real NV12 buffer that is read back to shared memory: it is
+// sampled by the GPU and read by the CPU. Allocating with GPU usage matters,
+// since a CPU-only Y8Cb8Cr8_420 allocation is free to come back as a planar
+// (YV12) layout, which this code path does not support.
+constexpr uint64_t kNV12ReadbackUsage =
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
+ AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN;
+
+// Allocates an NV12 AHardwareBuffer of `size`, or returns an empty handle if
+// the platform refuses the allocation (e.g. for odd dimensions).
+base::android::ScopedHardwareBufferHandle AllocateNV12Buffer(
+ const gfx::Size& size,
+ uint64_t usage = kNV12ReadbackUsage) {
+ AHardwareBuffer_Desc desc = {
+ .width = static_cast<uint32_t>(size.width()),
+ .height = static_cast<uint32_t>(size.height()),
+ .layers = 1,
+ .format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420,
+ .usage = usage,
+ };
+ AHardwareBuffer* buffer = nullptr;
+ AHardwareBuffer_allocate(&desc, &buffer);
+ if (!buffer) {
+ return base::android::ScopedHardwareBufferHandle();
+ }
+ return base::android::ScopedHardwareBufferHandle::Adopt(buffer);
+}
+
+// Whether the platform allocator hands back the interleaved NV12 plane layout
+// that CopyNativeBufferToSharedMemoryAsync() supports. Gralloc is free to pick
+// a planar layout instead, in which case the copy legitimately fails and the
+// tests below that expect success have nothing to assert.
+bool SupportsNV12PlaneLayout(AHardwareBuffer* buffer) {
+ AHardwareBuffer_Planes planes;
+ if (AHardwareBuffer_lockPlanes(buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN,
+ /*fence=*/-1, nullptr, &planes) != 0) {
+ return false;
+ }
+ const bool supported =
+ planes.planeCount == 3 && planes.planes[0].pixelStride == 1 &&
+ planes.planes[1].pixelStride == 2 && planes.planes[2].pixelStride == 2 &&
+ planes.planes[1].rowStride == planes.planes[2].rowStride &&
+ (static_cast<uint8_t*>(planes.planes[2].data) -
+ static_cast<uint8_t*>(planes.planes[1].data)) == 1;
+ AHardwareBuffer_unlock(buffer, nullptr);
+ return supported;
+}
+
+// Writes a distinct value into every byte the copy is supposed to read, and
+// returns those bytes in the order the destination is supposed to hold them:
+// `height` rows of `width` Y bytes, followed by `height / 2` rows of `width`
+// interleaved UV bytes. Returns an empty vector if the buffer can't be locked.
+std::vector<uint8_t> FillNV12Buffer(AHardwareBuffer* buffer,
+ const gfx::Size& size) {
+ AHardwareBuffer_Planes planes;
+ if (AHardwareBuffer_lockPlanes(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN,
+ /*fence=*/-1, nullptr, &planes) != 0) {
+ return {};
+ }
+
+ const size_t width = base::checked_cast<size_t>(size.width());
+ const size_t height = base::checked_cast<size_t>(size.height());
+ std::vector<uint8_t> expected;
+ auto fill_plane = [&](const AHardwareBuffer_Plane& src, size_t rows,
+ uint8_t seed) {
+ const size_t row_stride = src.rowStride;
+ // SAFETY: A locked plane holds `rows` rows at `rowStride` byte intervals,
+ // the last of which is at least `width` bytes long.
+ base::span<uint8_t> plane = UNSAFE_BUFFERS(base::span(
+ static_cast<uint8_t*>(src.data), (rows - 1) * row_stride + width));
+ for (size_t row = 0; row < rows; ++row) {
+ base::span<uint8_t> src_row = plane.subspan(row * row_stride, width);
+ for (size_t col = 0; col < width; ++col) {
+ src_row[col] = static_cast<uint8_t>(seed + row * 7 + col);
+ expected.push_back(src_row[col]);
+ }
+ }
+ };
+ fill_plane(planes.planes[0], height, /*seed=*/0);
+ fill_plane(planes.planes[1], height / 2, /*seed=*/128);
+
+ AHardwareBuffer_unlock(buffer, nullptr);
+ return expected;
+}
+
+// Regression test for the out-of-bounds write reachable from a compromised
+// renderer via the CopyNativeGmbToSharedMemoryAsync IPC. The destination size
+// used to be computed as `width * height * 3 / 2`, which rounds *down* for odd
+// dimensions, while libyuv::NV12Copy() writes ceil-rounded chroma planes. A
+// buffer with odd dimensions must be rejected outright.
+TEST(AHardwareBufferImageBackingFactoryCopyTest, RejectsOddDimensions) {
+ constexpr gfx::Size kOddSize(65, 65);
+ auto ahb_handle = AllocateNV12Buffer(kOddSize);
+ if (!ahb_handle.is_valid()) {
+ GTEST_SKIP() << "Odd-sized NV12 AHardwareBuffer allocation unsupported";
+ }
+
+ // This is the size that the old (truncating) check would have accepted; it is
+ // smaller than what NV12Copy() would write, so accepting it overflows.
+ const size_t undersized =
+ static_cast<size_t>(kOddSize.width()) * kOddSize.height() * 3 / 2;
+ ASSERT_LT(undersized, viz::SharedMemorySizeForSharedImageFormat(
+ viz::MultiPlaneFormat::kNV12, kOddSize)
+ .value());
+ auto region = base::UnsafeSharedMemoryRegion::Create(undersized);
+ ASSERT_TRUE(region.IsValid());
+
+ EXPECT_FALSE(
+ AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync(
+ gfx::GpuMemoryBufferHandle(std::move(ahb_handle)),
+ std::move(region)));
+}
+
+// A destination smaller than the full NV12 image must be rejected.
+TEST(AHardwareBufferImageBackingFactoryCopyTest, RejectsUndersizedDestination) {
+ constexpr gfx::Size kSize(64, 64);
+ auto ahb_handle = AllocateNV12Buffer(kSize);
+ if (!ahb_handle.is_valid()) {
+ GTEST_SKIP() << "NV12 AHardwareBuffer allocation unsupported";
+ }
+ const size_t required = viz::SharedMemorySizeForSharedImageFormat(
+ viz::MultiPlaneFormat::kNV12, kSize)
+ .value();
+ auto region = base::UnsafeSharedMemoryRegion::Create(required - 1);
+ ASSERT_TRUE(region.IsValid());
+
+ EXPECT_FALSE(
+ AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync(
+ gfx::GpuMemoryBufferHandle(std::move(ahb_handle)),
+ std::move(region)));
+}
+
+// A correctly sized destination must receive the whole image in the layout the
+// client expects: a tightly packed Y plane followed by a tightly packed UV
+// plane, both with a row size of `width` bytes.
+TEST(AHardwareBufferImageBackingFactoryCopyTest, CopiesBothPlanes) {
+ constexpr gfx::Size kSize(64, 64);
+ auto ahb_handle = AllocateNV12Buffer(
+ kSize, kNV12ReadbackUsage | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN);
+ if (!ahb_handle.is_valid()) {
+ GTEST_SKIP() << "NV12 AHardwareBuffer allocation unsupported";
+ }
+ if (!SupportsNV12PlaneLayout(ahb_handle.get())) {
+ GTEST_SKIP() << "Platform does not provide an interleaved NV12 layout";
+ }
+
+ const std::vector<uint8_t> expected = FillNV12Buffer(ahb_handle.get(), kSize);
+ ASSERT_EQ(expected.size(), viz::SharedMemorySizeForSharedImageFormat(
+ viz::MultiPlaneFormat::kNV12, kSize)
+ .value());
+
+ auto region = base::UnsafeSharedMemoryRegion::Create(expected.size());
+ ASSERT_TRUE(region.IsValid());
+ base::UnsafeSharedMemoryRegion region_for_reading = region.Duplicate();
+ ASSERT_TRUE(region_for_reading.IsValid());
+
+ ASSERT_TRUE(
+ AHardwareBufferImageBackingFactory::CopyNativeBufferToSharedMemoryAsync(
+ gfx::GpuMemoryBufferHandle(std::move(ahb_handle)),
+ std::move(region)));
+
+ base::WritableSharedMemoryMapping mapping = region_for_reading.Map();
+ ASSERT_TRUE(mapping.IsValid());
+ EXPECT_EQ(mapping.GetMemoryAsSpan<uint8_t>().first(expected.size()),
+ base::span(expected));
+}
+
} // anonymous namespace
} // namespace gpu
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page