diff --git a/include/TextureDataMapping.h b/include/TextureDataMapping.h index 09b89e49fd..99a8f50879 100644 --- a/include/TextureDataMapping.h +++ b/include/TextureDataMapping.h @@ -18,6 +18,14 @@ struct TextureBitField { size_t bit_range_start_index{ 0 }; // The index of the first bit of the field size_t bit_range_end_index{ 0 }; // The index of the last bit of the field + + //! A pixel value is decoded as a 32-bit field, so a well-formed range is ordered and within [0, 31]. The ranges come from untrusted texture + //! metadata, so this invariant must hold before use; an inverted or out-of-range range makes the bit-extraction shifts underflow and shift by 32 + //! or more bits, which is undefined behaviour. + [[nodiscard]] constexpr bool isValid() const noexcept + { + return bit_range_start_index <= bit_range_end_index && bit_range_end_index < 32; + } }; /*! diff --git a/src/TextureDataProvider.cpp b/src/TextureDataProvider.cpp index b6c8386142..80a537a7a4 100644 --- a/src/TextureDataProvider.cpp +++ b/src/TextureDataProvider.cpp @@ -28,6 +28,14 @@ std::optional TextureDataProvider::getValue(const size_t pixel_x, cons } const TextureBitField& bit_field = data_mapping_iterator->second; + + // The bit-field range originates from untrusted texture metadata. Reject anything that violates the + // TextureBitField invariant, so the shift counts below cannot underflow and shift the uint32_t by 32 or more bits (undefined behaviour). + if (! bit_field.isValid()) + { + return std::nullopt; + } + const uint32_t pixel_data = texture_->getPixel(pixel_x, pixel_y); // Extract relevant bits by rotating the pixel data left then right, which will insert 0s where appropriate diff --git a/src/utils/MeshUtils.cpp b/src/utils/MeshUtils.cpp index be7fc06422..6c940a2790 100644 --- a/src/utils/MeshUtils.cpp +++ b/src/utils/MeshUtils.cpp @@ -193,10 +193,22 @@ bool loadTextureFromPngData(const std::vector& texture_data, Mesh std::string feature_name = it->name.GetString(); const rapidjson::Value& array = it->value; - if (array.IsArray() && array.Size() == 2) + if (! array.IsArray() || array.Size() != 2 || ! array[0].IsUint() || ! array[1].IsUint()) { - (*texture_data_mapping)[feature_name] = TextureBitField{ array[0].GetUint(), array[1].GetUint() }; + spdlog::warn("Ignoring malformed texture data mapping for feature '{}' in {}", feature_name, source_description); + continue; } + + // The bit-field range is untrusted input. Enforce the TextureBitField invariant here, at the trust + // boundary, so every field that reaches the mapping (and therefore every consumer) is known-valid. + const TextureBitField bit_field{ array[0].GetUint(), array[1].GetUint() }; + if (! bit_field.isValid()) + { + spdlog::warn("Ignoring out-of-range texture bit field for feature '{}' in {}", feature_name, source_description); + continue; + } + + (*texture_data_mapping)[feature_name] = bit_field; } break; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6c61b9a3ba..696e286ff3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ set(TESTS_SRC_BASE LayerPlanTest PathOrderOptimizerTest PathOrderMonotonicTest + TextureDataProviderTest TimeEstimateCalculatorTest WallsComputationTest ) diff --git a/tests/TextureDataProviderTest.cpp b/tests/TextureDataProviderTest.cpp new file mode 100644 index 0000000000..e1ca8a0534 --- /dev/null +++ b/tests/TextureDataProviderTest.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#include "TextureDataProvider.h" + +#include + +#include + +#include "TextureDataMapping.h" +#include "mesh.h" + +namespace cura +{ + +// getValue() on a 1x1, 4-bytes-per-pixel texture holding the pixel 0xABCD1234 (big-endian byte order, +// matching Image::getPixel), for the given (untrusted) bit field. +static std::optional getValueFor(const TextureBitField& bit_field) +{ + auto texture = std::make_shared(1, 1, 4, std::vector{ 0xABu, 0xCDu, 0x12u, 0x34u }); + auto mapping = std::make_shared(); + (*mapping)["feature"] = bit_field; + return TextureDataProvider(nullptr, texture, mapping).getValue(0, 0, "feature"); +} + +// A valid, in-range bit field is accepted by the invariant and extracts exactly the requested bits, right-aligned. +TEST(TextureDataProviderTest, ValidBitFieldExtractsBits) +{ + EXPECT_TRUE((TextureBitField{ 0, 7 }.isValid())); + EXPECT_EQ(getValueFor(TextureBitField{ 0, 7 }), std::optional{ 0x34u }); // low byte + EXPECT_EQ(getValueFor(TextureBitField{ 8, 15 }), std::optional{ 0x12u }); // second byte + EXPECT_EQ(getValueFor(TextureBitField{ 24, 31 }), std::optional{ 0xABu }); // high byte +} + +// Out-of-range / inverted ranges come from untrusted texture metadata. isValid() is the gate the PNG-metadata parser applies +// at the trust boundary (so the field never enters the mapping); getValue() rejects them too, so no >= 32-bit shift (UB) can run. +TEST(TextureDataProviderTest, OutOfRangeBitFieldIsRejected) +{ + for (const TextureBitField& bad : { TextureBitField{ 0, 32 }, TextureBitField{ 0, 64 }, TextureBitField{ 10, 5 } }) + { + EXPECT_FALSE(bad.isValid()); + EXPECT_FALSE(getValueFor(bad).has_value()); + } +} + +} // namespace cura