From fb6dec697a2e89a6fdb13da8f1a79175b515502f Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Mon, 22 Jun 2026 15:19:54 +0100 Subject: [PATCH 1/5] Clamp out-of-range unorm inputs --- Source/UnitTest/test_encode.cpp | 169 ++++++++++++++++++++++++++++++++ Source/astcenc_image.cpp | 15 ++- 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/Source/UnitTest/test_encode.cpp b/Source/UnitTest/test_encode.cpp index 9e3aa45a..dcbf7dca 100644 --- a/Source/UnitTest/test_encode.cpp +++ b/Source/UnitTest/test_encode.cpp @@ -131,4 +131,173 @@ TEST(compress, data_buffer_exceeded) astcenc_context_free(context); } +/** @brief Helper to run a compression on a single 4x4 block */ +static void run_positive_test( + astcenc_profile profile, + float* input +) { + astcenc_error status; + astcenc_config config; + astcenc_context* context; + + static const astcenc_swizzle swizzle { + ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A + }; + + astcenc_config_init(profile, 4, 4, 1, ASTCENC_PRE_MEDIUM, 0, &config); + status = astcenc_context_alloc(&config, 1, &context, nullptr); + EXPECT_EQ(status, ASTCENC_SUCCESS); + + // Single 16 byte block output data + uint8_t data[16]; + + astcenc_image image; + image.dim_x = 4u; + image.dim_y = 4u; + image.dim_z = 1u; + image.data_type = ASTCENC_TYPE_F32; + float* slices = input; + image.data = reinterpret_cast(&slices); + + // Should not error or trip any UBSAN errors + status = astcenc_compress_image(context, &image, &swizzle, data, 16, 0); + EXPECT_EQ(status, ASTCENC_SUCCESS); + + astcenc_context_free(context); +} + +/** @brief Input data contains negative infinities. */ +TEST(compress, data_neg_inf_ldr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = -std::numeric_limits::infinity(); + + // Make data more complex to hit more compressor paths + input[4 + offset] = 0.75f; + input[8 + offset] = 0.35f; + input[12 + offset] = 0.0f; + + run_positive_test(ASTCENC_PRF_LDR, input.data()); + } +} + +/** @brief Input data contains negative infinities. */ +TEST(compress, data_neg_inf_hdr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = -std::numeric_limits::infinity(); + + run_positive_test(ASTCENC_PRF_HDR, input.data()); + } +} + +/** @brief Input data contains negative infinities. */ +TEST(compress, data_neg_inf_hdr_ldra) +{ + // Cycle though a single vad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = -std::numeric_limits::infinity(); + + run_positive_test(ASTCENC_PRF_HDR_RGB_LDR_A, input.data()); + } +} + +/** @brief Input data contains positive infinities. */ +TEST(compress, data_pos_inf_ldr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::infinity(); + + run_positive_test(ASTCENC_PRF_LDR, input.data()); + } +} + +/** @brief Input data contains positive infinities. */ +TEST(compress, data_pos_inf_hdr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::infinity(); + + run_positive_test(ASTCENC_PRF_HDR, input.data()); + } +} + +/** @brief Input data contains positive infinities. */ +TEST(compress, data_pos_inf_hdr_ldra) +{ + // Cycle though a single vad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::infinity(); + + run_positive_test(ASTCENC_PRF_HDR_RGB_LDR_A, input.data()); + } +} + + +/** @brief Input data contains NaN. */ +TEST(compress, data_nan_ldr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::quiet_NaN(); + + run_positive_test(ASTCENC_PRF_LDR, input.data()); + } +} + +/** @brief Input data contains NaN. */ +TEST(compress, data_nan_hdr) +{ + // Cycle though a single bad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::quiet_NaN(); + + run_positive_test(ASTCENC_PRF_HDR, input.data()); + } +} + +/** @brief Input data contains NaN. */ +TEST(compress, data_nan_hdr_ldra) +{ + // Cycle though a single vad value in each color channel + for(int offset = 0; offset < 4; offset++) + { + // 4x4 block input data + std::vector input(4 * 4 * 4, 0.5f); + input[0 + offset] = std::numeric_limits::quiet_NaN(); + + run_positive_test(ASTCENC_PRF_HDR_RGB_LDR_A, input.data()); + } +} + + + } diff --git a/Source/astcenc_image.cpp b/Source/astcenc_image.cpp index c32ba9d7..db825fa3 100644 --- a/Source/astcenc_image.cpp +++ b/Source/astcenc_image.cpp @@ -121,6 +121,8 @@ static vfloat4 swz_texel( /** * @brief Encode a texel that is entirely LDR linear. * + * Out-of-range inputs will be clamped to the valid UNORM range. + * * @param data The RGBA data to encode. * @param lns_mask The mask for the HDR channels than need LNS encoding. */ @@ -129,12 +131,19 @@ static vfloat4 encode_texel_unorm( vmask4 lns_mask ) { (void)lns_mask; - return data * 65535.0f; + vfloat4 raw_value = data * 65535.0f; + + // Unorm data must be in 0-1 range, so clamp because data can come from an + // unconstrained float. This will replace NaNs with zero. + return clamp(0.0f, 65535.0f, raw_value); } /** * @brief Encode a texel that includes at least some HDR LNS texels. * + * Out-of-range inputs will be clamped to the valid LNS or UNORM range, + * depending on @c lns_mask. + * * @param data The RGBA data to encode. * @param lns_mask The mask for the HDR channels than need LNS encoding. */ @@ -142,7 +151,9 @@ static vfloat4 encode_texel_lns( vfloat4 data, vmask4 lns_mask ) { - vfloat4 datav_unorm = data * 65535.0f; + vfloat4 datav_unorm = encode_texel_unorm(data, lns_mask); + + // Out-of-range inputs are clamped inside float_to_lns vfloat4 datav_lns = float_to_lns(data); return select(datav_unorm, datav_lns, lns_mask); } From 50c0d3e1cbafc333a044354b76064413eaf9ecb4 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Mon, 22 Jun 2026 15:28:21 +0100 Subject: [PATCH 2/5] Add changelog --- Docs/ChangeLog-5x.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/ChangeLog-5x.md b/Docs/ChangeLog-5x.md index 5e1cdf14..30b3c62b 100644 --- a/Docs/ChangeLog-5x.md +++ b/Docs/ChangeLog-5x.md @@ -18,6 +18,8 @@ The 5.6.0 release is a minor maintenance release. LDR image to a DDS container. * **Bug fix:** Fixed potential integer overflow when storing very large uncompressed images to a `dds` or `.ktx` output image format. + * **Bug fix:** Avoid undefined behavior caused by passing floating point + values outside of the [0.0, 1.0] range as data for a UNORM color channel. ## 5.5.0 From 1bdb6e536b1291cb167f386b2039d6dcb270ba9c Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Mon, 22 Jun 2026 15:29:48 +0100 Subject: [PATCH 3/5] Whitespace cleanup --- Source/UnitTest/test_encode.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/UnitTest/test_encode.cpp b/Source/UnitTest/test_encode.cpp index dcbf7dca..edff1701 100644 --- a/Source/UnitTest/test_encode.cpp +++ b/Source/UnitTest/test_encode.cpp @@ -298,6 +298,4 @@ TEST(compress, data_nan_hdr_ldra) } } - - } From 62dba0f48f2c3c04e833b1fee4b0a95c85d77ed8 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Mon, 22 Jun 2026 15:30:32 +0100 Subject: [PATCH 4/5] Typo fix --- Source/UnitTest/test_encode.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/UnitTest/test_encode.cpp b/Source/UnitTest/test_encode.cpp index edff1701..7392af13 100644 --- a/Source/UnitTest/test_encode.cpp +++ b/Source/UnitTest/test_encode.cpp @@ -202,7 +202,7 @@ TEST(compress, data_neg_inf_hdr) /** @brief Input data contains negative infinities. */ TEST(compress, data_neg_inf_hdr_ldra) { - // Cycle though a single vad value in each color channel + // Cycle though a single bad value in each color channel for(int offset = 0; offset < 4; offset++) { // 4x4 block input data @@ -244,7 +244,7 @@ TEST(compress, data_pos_inf_hdr) /** @brief Input data contains positive infinities. */ TEST(compress, data_pos_inf_hdr_ldra) { - // Cycle though a single vad value in each color channel + // Cycle though a single bad value in each color channel for(int offset = 0; offset < 4; offset++) { // 4x4 block input data @@ -287,7 +287,7 @@ TEST(compress, data_nan_hdr) /** @brief Input data contains NaN. */ TEST(compress, data_nan_hdr_ldra) { - // Cycle though a single vad value in each color channel + // Cycle though a single bad value in each color channel for(int offset = 0; offset < 4; offset++) { // 4x4 block input data From 34f8d234f0213cfcb77ab6b3fb6f38951cec4829 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Mon, 22 Jun 2026 16:54:10 +0100 Subject: [PATCH 5/5] Add changelog --- Docs/ChangeLog-5x.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/ChangeLog-5x.md b/Docs/ChangeLog-5x.md index 30b3c62b..81ecb96e 100644 --- a/Docs/ChangeLog-5x.md +++ b/Docs/ChangeLog-5x.md @@ -20,6 +20,8 @@ The 5.6.0 release is a minor maintenance release. uncompressed images to a `dds` or `.ktx` output image format. * **Bug fix:** Avoid undefined behavior caused by passing floating point values outside of the [0.0, 1.0] range as data for a UNORM color channel. + * **Bug fix:** Avoid undefined behavior caused by unaligned access in SSE + and AVX2 SIMD library implementations. ## 5.5.0