Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/TextureDataMapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

/*!
Expand Down
8 changes: 8 additions & 0 deletions src/TextureDataProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ std::optional<uint32_t> 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
Expand Down
16 changes: 14 additions & 2 deletions src/utils/MeshUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,22 @@ bool loadTextureFromPngData(const std::vector<unsigned char>& 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;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ set(TESTS_SRC_BASE
LayerPlanTest
PathOrderOptimizerTest
PathOrderMonotonicTest
TextureDataProviderTest
TimeEstimateCalculatorTest
WallsComputationTest
)
Expand Down
46 changes: 46 additions & 0 deletions tests/TextureDataProviderTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2025 UltiMaker
// CuraEngine is released under the terms of the AGPLv3 or higher

#include "TextureDataProvider.h"

#include <memory>

#include <gtest/gtest.h>

#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<uint32_t> getValueFor(const TextureBitField& bit_field)
{
auto texture = std::make_shared<Image>(1, 1, 4, std::vector<uint8_t>{ 0xABu, 0xCDu, 0x12u, 0x34u });
auto mapping = std::make_shared<TextureDataMapping>();
(*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<uint32_t>{ 0x34u }); // low byte
EXPECT_EQ(getValueFor(TextureBitField{ 8, 15 }), std::optional<uint32_t>{ 0x12u }); // second byte
EXPECT_EQ(getValueFor(TextureBitField{ 24, 31 }), std::optional<uint32_t>{ 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
Loading