diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc index 915fe5adf8d50..9054db896e37b 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_framebuffers.cc @@ -14,6 +14,10 @@ #include "base/containers/heap_array.h" #include "base/memory/raw_ptr.h" #include "base/strings/string_number_conversions.h" +#if defined(ADDRESS_SANITIZER) +#include +#endif +#include "third_party/abseil-cpp/absl/cleanup/cleanup.h" #include "gpu/command_buffer/common/gles2_cmd_format.h" #include "gpu/command_buffer/common/gles2_cmd_utils.h" #include "gpu/command_buffer/service/context_group.h" @@ -828,6 +832,93 @@ TEST_P(GLES3DecoderTest, ReadPixels2PixelPackBuffer) { EXPECT_EQ(GL_NO_ERROR, GetGLError()); } +// Regression test: read_pixel_format accepts GL_R16_EXT/GL_RG16_EXT/ +// GL_RGBA16_EXT (sized internal formats) but GLES2Util::ElementsPerGroup +// returns 0 for them, so ComputeImageDataSizesES3 succeeds with size = 0. +// HandleReadPixels then validates the shm region for 0 bytes, gets a pointer +// at the renderer-chosen offset (including one-past-end), and hands it to the +// native driver, which writes width*height*bpp bytes -> OOB write past the +// transfer buffer in the GPU process. +TEST_P(GLES3DecoderManualInitTest, ReadPixelsR16ExtZeroSizeValidationOOB) { + InitState init; + init.extensions = "GL_EXT_texture_norm16"; + init.gl_version = "OpenGL ES 3.0"; + init.context_type = CONTEXT_TYPE_OPENGLES3; + InitDecoder(init); + + // Step 1: directly demonstrate the validator/size-calculator desync. + uint32_t computed = 0xdeadbeef; + EXPECT_TRUE(GLES2Util::ComputeImageDataSizes( + 64, 64, 1, GL_R16_EXT, GL_UNSIGNED_SHORT, 4, &computed, nullptr, + nullptr)); + EXPECT_EQ(0u, computed) + << "ComputeImageDataSizes returned non-zero; bug already fixed"; + + // Step 2: send a hostile ReadPixels that a compromised renderer could craft. + // pixels_shm_offset = end-of-buffer; the decoder validates 0 bytes so this + // passes and returns base + kSharedBufferSize (one-past-end). + surface_->SetSize(gfx::Size(INT_MAX, INT_MAX)); + const GLsizei kWidth = 32; + const GLsizei kHeight = 32; + // Native R16 + UNSIGNED_SHORT = 2 bytes/pixel -> 32*32*2 = 2048 bytes + // written by the driver, all of it past the validated buffer end. + const uint32_t kPixelsOffset = kSharedBufferSize; // one-past-end + +#if defined(ADDRESS_SANITIZER) + // The transfer buffer is mmap'd shared memory (no ASAN redzones). Poison the + // page slack past the logical buffer end so ASAN deterministically reports + // the OOB write instead of relying on whatever mapping happens to follow. + // kSharedBufferSize = 2048; the mapping is at least one 4096-byte page, so + // [base+2048, base+4096) is mapped slack we can safely poison. + uint8_t* poison_at = UNSAFE_TODO( + static_cast(shared_memory_base_.get()) + kSharedBufferSize); + static_assert(kSharedBufferSize == 2048); + const size_t kPoisonLen = 2048; + ASAN_POISON_MEMORY_REGION(poison_at, kPoisonLen); + absl::Cleanup unpoison = [poison_at] { + ASAN_UNPOISON_MEMORY_REGION(poison_at, kPoisonLen); + }; +#endif + + // The decoder falls through to the native-driver IMPLEMENTATION_COLOR_READ_* + // query because (GL_R16_EXT, GL_UNSIGNED_SHORT) is not in the hardcoded + // accepted list. Simulate an Android native driver that returns the sized + // enum (the precondition documented in the report). + EXPECT_CALL(*gl_, GetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, _)) + .Times(AnyNumber()) + .WillRepeatedly(SetArgPointee<1>(GL_R16_EXT)); + EXPECT_CALL(*gl_, GetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, _)) + .Times(AnyNumber()) + .WillRepeatedly(SetArgPointee<1>(GL_UNSIGNED_SHORT)); + EXPECT_CALL(*gl_, GetError()) + .Times(AnyNumber()) + .WillRepeatedly(Return(GL_NO_ERROR)); + + // The "native driver" computes its own write size for R16 + UNSIGNED_SHORT + // (2 bytes/pixel) and writes into the pointer the decoder validated for 0 + // bytes. With kWidth*kHeight*2 = 8192 bytes starting at one-past-end of a + // 2048-byte transfer buffer, this is a hard OOB write past the shm mapping. + EXPECT_CALL( + *gl_, ReadPixels(0, 0, kWidth, kHeight, GL_R16_EXT, GL_UNSIGNED_SHORT, _)) + .Times(AnyNumber()) + .WillRepeatedly([](GLint, GLint, GLsizei w, GLsizei h, GLenum, GLenum, + void* pixels) { + // What a native R16 glReadPixels does: write w*h*2 bytes. + UNSAFE_TODO(memset(pixels, 0xAB, static_cast(w) * h * 2u)); + }); + + cmds::ReadPixels cmd; + cmd.Init(0, 0, kWidth, kHeight, GL_R16_EXT, GL_UNSIGNED_SHORT, + shared_memory_id_, kPixelsOffset, /*result_shm_id=*/0, + /*result_shm_offset=*/0, /*async=*/false); + // OOB write fires inside this call: ASAN reports a 2048-byte write past the + // end of the transfer buffer (the region the decoder validated for 0 bytes). + // After the bandaid fix, the command is rejected with GL_INVALID_OPERATION + // before reaching glReadPixelsFn. + ExecuteCmd(cmd); + GetGLError(); // Consume the INVALID_OPERATION emitted by the fixed decoder. +} + TEST_P(GLES3DecoderTest, ReadPixelsPixelPackBufferMapped) { const GLsizei kWidth = 5; const GLsizei kHeight = 3;