blob: 7abfbc32d155a10b67a27ed31d4a5670dc9ecf60 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gpu/command_buffer/common/iosurface_validation.h"
#include <CoreVideo/CoreVideo.h>
#include <IOSurface/IOSurfaceRef.h>
namespace gpu {
uint32_t SharedImageFormatToIOSurfacePixelFormat(viz::SharedImageFormat format,
bool override_rgba_to_bgra) {
if (format.is_single_plane()) {
if (format == viz::SinglePlaneFormat::kR_8) {
return kCVPixelFormatType_OneComponent8;
} else if (format == viz::SinglePlaneFormat::kRG_88) {
return kCVPixelFormatType_TwoComponent8;
} else if (format == viz::SinglePlaneFormat::kR_16) {
return kCVPixelFormatType_OneComponent16;
} else if (format == viz::SinglePlaneFormat::kRG_1616) {
return kCVPixelFormatType_TwoComponent16;
} else if (format == viz::SinglePlaneFormat::kBGRA_1010102) {
return kCVPixelFormatType_ARGB2101010LEPacked;
} else if (format == viz::SinglePlaneFormat::kBGRA_8888 ||
format == viz::SinglePlaneFormat::kBGRX_8888) {
return kCVPixelFormatType_32BGRA;
} else if (format == viz::SinglePlaneFormat::kRGBA_8888 ||
format == viz::SinglePlaneFormat::kRGBX_8888) {
return override_rgba_to_bgra ? kCVPixelFormatType_32BGRA
: kCVPixelFormatType_32RGBA;
} else if (format == viz::SinglePlaneFormat::kRGBA_F16) {
return kCVPixelFormatType_64RGBAHalf;
} else if (format == viz::SinglePlaneFormat::kBGR_565 ||
format == viz::SinglePlaneFormat::kRGBA_4444 ||
format == viz::SinglePlaneFormat::kRGBA_1010102) {
// Technically RGBA_1010102 should be accepted as 'R10k', but
// then it won't be supported by CGLTexImageIOSurface2D(), so
// it's best to reject it here.
return 0;
}
} else if (format.is_multi_plane()) {
if (format == viz::MultiPlaneFormat::kNV12) {
return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kNV16) {
return kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kNV24) {
return kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kNV12A) {
return kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar;
} else if (format == viz::MultiPlaneFormat::kP010) {
return kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kP210) {
return kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kP410) {
return kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange;
} else if (format == viz::MultiPlaneFormat::kI420) {
return kCVPixelFormatType_420YpCbCr8Planar;
} else if (format == viz::MultiPlaneFormat::kYV12) {
return 0;
}
}
NOTREACHED();
}
bool ValidateIOSurface(const gfx::ScopedIOSurface& io_surface,
viz::SharedImageFormat format,
gfx::Size size,
std::string* out_error_str) {
// Validate top-level IOSurface format.
uint32_t io_surface_format = IOSurfaceGetPixelFormat(io_surface.get());
// Always treat RGBA as BGRA. It makes no difference for validating size since
// they have the same size, and avoids the complexity of actually trying to
// figure out if conversion should happen.
if (io_surface_format == kCVPixelFormatType_32RGBA) {
io_surface_format = kCVPixelFormatType_32BGRA;
}
const bool override_rgba_to_bgra = true;
if (io_surface_format !=
SharedImageFormatToIOSurfacePixelFormat(format, override_rgba_to_bgra)) {
if (out_error_str) {
*out_error_str =
"IOSurface pixel format does not match specified shared "
"image format.";
}
return false;
}
// Validate top-level IOSurface dimensions.
if (IOSurfaceGetWidth(io_surface.get()) !=
static_cast<size_t>(size.width()) ||
IOSurfaceGetHeight(io_surface.get()) !=
static_cast<size_t>(size.height())) {
if (out_error_str) {
*out_error_str = "IOSurface size does not match specified size.";
}
return false;
}
// Ensure the IOSurface has at least as many planes as the requested format.
// For single-planar IOSurfaces, IOSurfaceGetPlaneCount returns 0.
size_t io_surface_plane_count =
std::max<size_t>(1, IOSurfaceGetPlaneCount(io_surface.get()));
if (io_surface_plane_count < static_cast<size_t>(format.NumberOfPlanes())) {
if (out_error_str) {
*out_error_str = "IOSurface plane count is too small.";
}
return false;
}
// Validate per-plane dimensions and stride. A malformed IOSurface could
// have planes with dimensions inconsistent with its top-level size and
// format, leading to out-of-bounds access during buffer operations.
for (int plane_index = 0; plane_index < format.NumberOfPlanes();
++plane_index) {
gfx::Size plane_size = format.GetPlaneSize(plane_index, size);
if (IOSurfaceGetWidthOfPlane(io_surface.get(), plane_index) !=
static_cast<size_t>(plane_size.width()) ||
IOSurfaceGetHeightOfPlane(io_surface.get(), plane_index) !=
static_cast<size_t>(plane_size.height())) {
if (out_error_str) {
*out_error_str = "IOSurface plane size does not match specified size.";
}
return false;
}
// Ensure the IOSurface has enough bytes per row for the plane to prevent
// potential out-of-bounds access when copying or accessing the buffer.
size_t io_surface_bytes_per_row =
IOSurfaceGetBytesPerRowOfPlane(io_surface.get(), plane_index);
size_t min_bytes_per_row;
if (format.is_single_plane()) {
CHECK(!format.IsCompressed());
min_bytes_per_row = static_cast<size_t>(format.BytesPerPixel()) *
static_cast<size_t>(plane_size.width());
} else {
min_bytes_per_row =
static_cast<size_t>(format.MultiplanarStorageBytesPerChannel()) *
static_cast<size_t>(format.NumChannelsInPlane(plane_index)) *
static_cast<size_t>(plane_size.width());
}
if (io_surface_bytes_per_row < min_bytes_per_row) {
if (out_error_str) {
*out_error_str = "IOSurface bytes per row is too small.";
}
return false;
}
}
return true;
}
} // namespace gpu