From b7860407f008400624cf2237990edde8207376bd Mon Sep 17 00:00:00 2001 From: Arnesh Banerjee Date: Tue, 28 Jul 2026 16:19:17 +0530 Subject: [PATCH] imgcodecs: fix pointer overflow in 16u icvCvt helpers The 16-bit icvCvt_* helpers in utils.cpp move the row pointer with step/sizeof(x[0]) - size.width*N. sizeof is unsigned, so this is done in unsigned math. When the caller passes step == 0, it underflows to a huge value and the pointer goes out of range, which is undefined behavior. The TIFF decoder hits this: it walks rows itself and calls these helpers with step = 0, so reading a 16-bit 4-channel TIFF triggers the bug. Cast sizeof(...) to int so the math is signed. Same result for normal calls, no overflow when the step is 0. Added Imgcodecs_Tiff.regression_29615_16UC4 which round-trips a CV_16UC4 TIFF in memory. This is the 4.x version of the fix, requested in https://github.com/opencv/opencv/pull/29620 (issue #29615). --- modules/imgcodecs/src/utils.cpp | 12 ++++++------ modules/imgcodecs/test/test_tiff.cpp | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/modules/imgcodecs/src/utils.cpp b/modules/imgcodecs/src/utils.cpp index 5c3127792a..f0a3af2139 100644 --- a/modules/imgcodecs/src/utils.cpp +++ b/modules/imgcodecs/src/utils.cpp @@ -169,13 +169,13 @@ void icvCvt_Gray2BGR_16u_C1C3R( const ushort* gray, int gray_step, ushort* bgr, int bgr_step, Size size ) { int i; - for( ; size.height--; gray += gray_step/sizeof(gray[0]) ) + for( ; size.height--; gray += gray_step/(int)sizeof(gray[0]) ) { for( i = 0; i < size.width; i++, bgr += 3 ) { bgr[0] = bgr[1] = bgr[2] = gray[i]; } - bgr += bgr_step/sizeof(bgr[0]) - size.width*3; + bgr += bgr_step/(int)sizeof(bgr[0]) - size.width*3; } } @@ -214,8 +214,8 @@ void icvCvt_BGRA2BGR_16u_C4C3R( const ushort* bgra, int bgra_step, bgr[0] = t0; bgr[1] = t1; t0 = bgra[swap_rb^2]; bgr[2] = t0; } - bgr += bgr_step/sizeof(bgr[0]) - size.width*3; - bgra += bgra_step/sizeof(bgra[0]) - size.width*4; + bgr += bgr_step/(int)sizeof(bgr[0]) - size.width*3; + bgra += bgra_step/(int)sizeof(bgra[0]) - size.width*4; } } @@ -252,8 +252,8 @@ void icvCvt_BGRA2RGBA_16u_C4R( const ushort* bgra, int bgra_step, rgba[0] = t2; rgba[1] = t1; rgba[2] = t0; rgba[3] = t3; } - bgra += bgra_step/sizeof(bgra[0]) - size.width*4; - rgba += rgba_step/sizeof(rgba[0]) - size.width*4; + bgra += bgra_step/(int)sizeof(bgra[0]) - size.width*4; + rgba += rgba_step/(int)sizeof(rgba[0]) - size.width*4; } } diff --git a/modules/imgcodecs/test/test_tiff.cpp b/modules/imgcodecs/test/test_tiff.cpp index 400e9a9a15..ca0d9fa93b 100644 --- a/modules/imgcodecs/test/test_tiff.cpp +++ b/modules/imgcodecs/test/test_tiff.cpp @@ -689,6 +689,28 @@ TEST(Imgcodecs_Tiff, readWrite_unsigned) EXPECT_EQ(0, remove(filenameOutput.c_str())); } +// See https://github.com/opencv/opencv/issues/29615 +// Decoding a 16-bit 4-channel TIFF used to form an out of range pointer in +// icvCvt_BGRA2RGBA_16u_C4R because the byte step was divided by an unsigned +// sizeof and then had size.width*4 subtracted, which wraps around for a zero +// step. This just checks that a 16UC4 TIFF round-trips correctly; the value is +// mainly that the sanitizer builds no longer report the pointer overflow. +TEST(Imgcodecs_Tiff, regression_29615_16UC4) +{ + Mat img(4, 3, CV_16UC4); + randu(img, Scalar::all(0), Scalar::all(65535)); + + vector buf; + ASSERT_NO_THROW(ASSERT_TRUE(imencode(".tiff", img, buf))); + + Mat decoded; + ASSERT_NO_THROW(decoded = imdecode(buf, IMREAD_UNCHANGED)); + ASSERT_FALSE(decoded.empty()); + ASSERT_EQ(CV_16UC4, decoded.type()); + ASSERT_EQ(img.size(), decoded.size()); + EXPECT_EQ(0, cvtest::norm(img, decoded, NORM_INF)); +} + TEST(Imgcodecs_Tiff, readWrite_32FC1) { const string root = cvtest::TS::ptr()->get_data_path();