diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 11cb22f514..3a4d973843 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -2897,6 +2897,106 @@ TEST_P(Texture2DTestES3, StaleFormatCacheOutOrRangeMip) EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); } +// Regression test: redefining a texture from RGB to R8 leaves stale formatID +// on mip levels created by retainImageDefinitions(). texSubImage2D on a stale +// mip selects LoadToNative3To4 (reads 3 bytes/pixel) for a 1 byte/pixel R8 +// source buffer, causing a heap-buffer-overflow READ. +TEST_P(Texture2DTestES3, StaleFormatIDHeapOverflowRead) +{ + GLTexture tex; + glBindTexture(GL_TEXTURE_2D, tex); + + // Step 1: Define as RGB 128x128 + generate mipmaps + // This creates mNativeTextureStorage with R8G8B8 format and full mip chain. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 128, 128, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GL_NO_ERROR(); + + // Step 2: Redefine as R8 64x64 + generate mipmaps + // redefineImage() calls deallocateNativeStorage(keepImages=true) which calls + // retainImageDefinitions(): stamps formatID=R8G8B8 on all mip levels. + // Only level 0 is updated to R8. Mip levels 1+ keep stale R8G8B8 formatID. + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 64, 64, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GL_NO_ERROR(); + + // Step 3: texSubImage2D on mip level 1 (32x32) with R8 data. + // setPerSliceSubImage sees stale formatID=R8G8B8, selects LoadToNative3To4 + // which reads 3 bytes/pixel from a 1 byte/pixel source -> heap overread. + const GLsizei w = 32, h = 32; + std::vector data(w * h, 0x41); + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, w, h, GL_RED, GL_UNSIGNED_BYTE, data.data()); + ASSERT_GL_NO_ERROR(); + + // Step 4: Read back to verify: OOB data would appear in pixel values. + GLFramebuffer fbo; + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) + { + std::vector rgba(w * h * 4, 0); + glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + + // Count pixels that contain OOB data (not our 0x41 fill, not zero) + int oobPixels = 0; + for (int i = 0; i < w * h; i++) + { + if (rgba[i * 4] != 0x41 && rgba[i * 4] != 0x00) + { + oobPixels++; + } + } + // If the bug is present, oobPixels > 0 means heap data leaked. + // On a fixed build, all R-channel values should be 0x41. + fprintf(stderr, "OOB pixels: %d / %d\n", oobPixels, w * h); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +// Regression test: redefining a texture to a larger size (same format) leaves stale +// imageDef.image pointing to the old smaller Metal texture. texSubImage2D passes GL +// validation (which sees the new large dimensions) but writes to the stale small +// texture via replaceRegion: OOB write. Pure size mismatch, no format change needed. +// +// With MTL_DEBUG_LAYER=1: +// _validateReplaceRegion: failed assertion +// '(origin.x + size.width)(128) must be <= width(8).' +TEST_P(Texture2DTestES3, StaleImageSizeHeapOverflowWrite) +{ + GLTexture tex; + glBindTexture(GL_TEXTURE_2D, tex); + + // Step 1: RGBA 16×16 + mipmaps -> native storage with mip chain: 16, 8, 4, 2, 1 + const GLsizei smallSize = 16; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, smallSize, smallSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GL_NO_ERROR(); + + // Step 2: RGBA 256×256 + mipmaps: same format, larger size. + // redefineImage detects size mismatch -> deallocateNativeStorage(keepImages=true) + // -> retainImageDefinitions() creates views of OLD 16×16 native on all mip levels. + // Only level 0 updated to 256×256. ensureNativeStorageCreated creates new 256×256 + // native but stale mip images (8×8, 4×4, ...) don't match -> NOT invalidated. + const GLsizei largeSize = 256; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, largeSize, largeSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GL_NO_ERROR(); + + // Step 3: texSubImage2D at mip level 1. + // GL sees level 1 as 128×128 -> validation passes. + // But imageDef[1].image is the stale 8×8 texture. + // replaceRegion writes 128×128×4 = 65536 bytes to 8×8×4 = 256 byte texture: OOB. + const GLsizei writeW = largeSize / 2, writeH = largeSize / 2; + std::vector attackData(writeW * writeH * 4, 0xDE); + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, writeW, writeH, GL_RGBA, GL_UNSIGNED_BYTE, + attackData.data()); + ASSERT_GL_NO_ERROR(); +} + // Almost mirrors UnitTest_DMSAA_dst_read test from Android skqp test suite TEST_P(Texture2DTestES3, UnitTest_DMSAA_dst_read) {