Compare commits

..
Author SHA1 Message Date
ExPikaPaka ae3e41eb02 Add a regression test for a cyclic component reference
Stores a painted cube, points its component back at the object that holds it and
expects the load to fail. Without the bound the test does not finish: the work
list grows until the process is killed.
2026-10-01 09:14:46 +02:00
ExPikaPaka 92ab583ecc Reject 3MF component references that form a cycle
_generate_current_object_list expands component references through a work list
with no bound. An object whose component points back at itself, or a pair that
point at each other, makes the list grow until the process runs out of memory:
a few hundred bytes of XML take the slicer past 20 GB of resident size.

Bound the expansion by the number of objects in the file. A reference chain
longer than that has to revisit an object, so this rejects every cycle and no
acyclic file, however deeply nested. A second bound on the number of expanded
components stops an acyclic graph that fans out exponentially.
2026-10-01 08:50:22 +02:00
7 changed files with 156 additions and 156 deletions
+2 -9
View File
@@ -809,20 +809,13 @@ void priv::set_skip_for_out_of_aoi(std::vector<bool> &skip_indicies,
}); // END parallel for
// inspect all triangles, when it is out of bounding box
// NOTE: std::vector<bool> is bit packed, thus setting its items from multiple threads is a
// read-modify-write race on the shared words and silently loses flags. Collect the flags into
// a byte per triangle, where the chunks do not share memory, and merge them afterwards.
std::vector<unsigned char> skip_triangle(its.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, its.indices.size()),
[&its, &is_on_sides, &skip_triangle](const tbb::blocked_range<size_t> &range) {
[&its, &is_on_sides, &skip_indicies](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
if (is_all_on_one_side(its.indices[i], is_on_sides))
skip_triangle[i] = 1;
skip_indicies[i] = true;
}
}); // END parallel for
for (size_t i = 0; i < skip_triangle.size(); ++i)
if (skip_triangle[i])
skip_indicies[i] = true;
}
indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its,
+25 -9
View File
@@ -1340,7 +1340,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_relationship(const char** attributes, unsigned int num_attributes);
void _generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
bool _generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
bool _generate_volumes_new(ModelObject& object, const std::vector<Component> &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
//bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
@@ -2055,7 +2055,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
std::vector<Component> object_id_list;
_generate_current_object_list(object_id_list, object.first, m_current_objects);
if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -2154,7 +2155,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}*/
std::vector<Component> object_id_list;
_generate_current_object_list(object_id_list, object.first, m_current_objects);
if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -5002,31 +5004,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
void _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap &current_objects)
bool _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap &current_objects)
{
std::list<std::pair<Component, Transform3d>> id_list;
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
// A chain of component references longer than the number of objects has to visit an object
// twice, so the component graph contains a cycle and the expansion below would not stop.
const size_t max_depth = current_objects.size();
// An acyclic graph may still expand exponentially, so bound the number of expanded components
// as well. Way above the number of parts of any real object.
static constexpr size_t max_components = 100000;
std::list<std::tuple<Component, Transform3d, size_t>> id_list;
id_list.push_back(std::make_tuple(Component(object_id, Transform3d::Identity()), Transform3d::Identity(), 0));
size_t num_components = 0;
while (!id_list.empty())
{
auto current_item = id_list.front();
Component current_id = current_item.first;
Component current_id = std::get<0>(current_item);
id_list.pop_front();
if (std::get<2>(current_item) > max_depth || ++ num_components > max_components) {
add_error("invalid 3mf: cyclic or too deeply nested components");
sub_objects.clear();
return false;
}
IdToCurrentObjectMap::iterator current_object = current_objects.find(current_id.object_id);
if (current_object != current_objects.end()) {
//found one
if (!current_object->second.components.empty()) {
for (const Component &comp : current_object->second.components) {
id_list.push_back(std::pair(comp, current_item.second * comp.transform));
id_list.push_back(std::make_tuple(comp, std::get<1>(current_item) * comp.transform, std::get<2>(current_item) + 1));
}
}
else if (!(current_object->second.geometry.empty())) {
//CurrentObject* ptr = &(current_objects[current_id]);
//CurrentObject* ptr2 = &(current_object->second);
sub_objects.push_back({ current_object->first, current_item.second});
sub_objects.push_back({ current_object->first, std::get<1>(current_item)});
}
}
}
return true;
}
bool _BBS_3MF_Importer::_generate_volumes_new(ModelObject& object, const std::vector<Component> &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions)
+45 -56
View File
@@ -1378,66 +1378,55 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out;
};
// The layers are processed in groups of "granularity" layers. A layer projects its shells up to "granularity"
// layers away, thus a group may write into the slots of its neighbor groups. The even and the odd groups
// therefore write into two disjoint halves of the output vectors (the 2nd half is offset by num_layers) and
// both halves are merged below. The group index has to be derived from the layer index and not from the extent
// of the TBB sub-range: tbb::blocked_range bisects at midpoints, thus a sub-range neither starts at a multiple
// of the grain size nor covers a whole group, and two sub-ranges of one group would append into a single
// ExPolygons concurrently. Iterating over the groups keeps every group on a single thread, in ascending order.
const size_t num_groups = (num_layers + size_t(granularity) - 1) / size_t(granularity);
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_groups, 1), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
for (size_t group_idx = range.begin(); group_idx < range.end(); ++ group_idx) {
const size_t layer_idx_offset = (group_idx & 1) * num_layers;
const size_t layer_idx_begin = group_idx * size_t(granularity);
const size_t layer_idx_end = std::min(num_layers, layer_idx_begin + size_t(granularity));
for (size_t layer_idx = layer_idx_begin; layer_idx < layer_idx_end; ++ layer_idx) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) {
// Clean up thin projections. They are not printable anyways.
top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last));
}
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers, granularity), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
size_t group_idx = range.begin() / granularity;
size_t layer_idx_offset = (group_idx & 1) * num_layers;
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) {
// Clean up thin projections. They are not printable anyways.
top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last));
}
}
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) {
// Clean up thin projections. They are not printable anyways.
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last));
}
}
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) {
// Clean up thin projections. They are not printable anyways.
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last));
}
}
}
}
}
}
});
+1 -15
View File
@@ -424,16 +424,8 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
[this](size_t i, size_t j) { return m_layer_outlines[i].second.size() < m_layer_outlines[j].second.size(); });
// Layer range for which the collisions will be calculated.
// Another thread may have advanced getMaxCalculatedLayer() past max_layer_idx after this calculation
// was requested. Bail out in that case, otherwise the layer range would be negative and allocating
// it would throw std::length_error out of a parallel task.
const LayerIndex start_layer = 1 + m_collision_cache.getMaxCalculatedLayer(radius);
if (start_layer > max_layer_idx) {
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
return;
}
LayerPolygonCache data;
data.allocate(start_layer, max_layer_idx + 1);
data.allocate(m_collision_cache.getMaxCalculatedLayer(radius) + 1, max_layer_idx + 1);
const bool calculate_placable = m_support_rests_on_model && radius == 0;
LayerPolygonCache data_placeable;
@@ -812,12 +804,6 @@ void TreeModelVolumes::calculateWallRestrictions(const std::vector<RadiusLayerPa
const coord_t radius = keys[key_idx].first;
const LayerIndex max_required_layer = keys[key_idx].second;
const coord_t min_layer_bottom = std::max(1, m_wall_restrictions_cache.getMaxCalculatedLayer(radius));
if (min_layer_bottom > max_required_layer) {
// Another thread has calculated this range in the meantime. Continuing would make
// buffer_size negative and allocating it would throw std::length_error.
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
continue;
}
const size_t buffer_size = max_required_layer + 1 - min_layer_bottom;
std::vector<Polygons> data(buffer_size, Polygons{});
std::vector<Polygons> data_min;
+57 -59
View File
@@ -12,7 +12,6 @@
#include <thread>
#include <tbb/parallel_for.h>
#include <tbb/task_arena.h>
#include <tbb/task_scheduler_observer.h>
#include "Thread.hpp"
#include "Utils.hpp"
@@ -213,71 +212,70 @@ bool is_main_thread_active()
return get_main_thread_id() == boost::this_thread::get_id();
}
// Name the current TBB worker thread and set its locale to "C", so that the G-code generator
// produces "." as a decimal separator. Called once per worker thread, before it runs its first task.
static void setup_tbb_worker_thread()
{
static std::atomic<size_t> s_worker_idx{ 0 };
std::ostringstream name;
name << "slic3r_tbb_" << (1 + s_worker_idx.fetch_add(1, std::memory_order_relaxed));
set_current_thread_name(name.str().c_str());
#ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
std::setlocale(LC_ALL, "C");
#else
// We are leaking some memory here, because the newlocale() produced memory will never be released.
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
uselocale(newlocale(
#ifdef __APPLE__
LC_ALL_MASK
#else // some Unix / Linux / BSD
LC_ALL
#endif
, "C", nullptr));
#endif
}
// Sets up the TBB worker threads of the arena of the thread, which activated the observation.
// A worker sets itself up on entry to the arena, before it executes its first task, thus unlike a barrier
// inside a parallel_for, this does not depend on TBB running any number of tasks simultaneously.
class TBBWorkerThreadSetupObserver : public tbb::task_scheduler_observer
{
public:
TBBWorkerThreadSetupObserver() { this->observe(true); }
void on_scheduler_entry(bool is_worker) override
{
// Leave the external threads (the calling / UI thread) alone, their name and locale must not be modified here.
if (! is_worker)
return;
// A worker thread enters an arena many times, while its name and locale have to be set just once.
static thread_local bool initialized = false;
if (initialized)
return;
initialized = true;
setup_tbb_worker_thread();
}
};
// Name the threads of the Intel TBB thread pool by an index and set their locale to "C"
// for the G-code generator to produce "." as a decimal separator.
// Formerly all the worker threads were caught inside a single parallel_for, which was held on a condition
// variable barrier until max_concurrency() of its chunks were running. TBB guarantees no such simultaneity,
// thus the barrier was able to block the slicing threads indefinitely. The TBB scheduler observer below
// sets each worker up on its own, thus no two chunks have to run at the same time.
// Spawn (n - 1) worker threads on Intel TBB thread pool and name them by an index and a system thread ID.
// Also it sets locale of the worker threads to "C" for the G-code generator to produce "." as a decimal separator.
void name_tbb_thread_pool_threads_set_locale()
{
static bool initialized = false;
if (initialized)
return;
initialized = true;
// see GH issue #5661 PrusaSlicer hangs on Linux when run with non standard task affinity
// TBB will respect the task affinity mask on Linux and spawn less threads than std::thread::hardware_concurrency().
// const size_t nthreads_hw = std::thread::hardware_concurrency();
const size_t nthreads_hw = tbb::this_task_arena::max_concurrency();
size_t nthreads = nthreads_hw;
#ifdef SLIC3R_PROFILE
// Shiny profiler is not thread safe, thus disable parallelization.
disable_multi_threading();
nthreads = 1;
#endif
// An observer is local to the arena of the thread which activates it, thus one observer is registered
// per calling thread. Being function local and thread local, it is also initialized exactly once per
// thread without a race. It is intentionally never destroyed, as it has to stay alive as long as the
// TBB scheduler may notify it, which includes the shutdown of the process.
static thread_local tbb::task_scheduler_observer *observer = new TBBWorkerThreadSetupObserver();
(void)observer;
size_t nthreads_running(0);
std::condition_variable cv;
std::mutex cv_m;
auto master_thread_id = std::this_thread::get_id();
tbb::parallel_for(
tbb::blocked_range<size_t>(0, nthreads, 1),
[&nthreads_running, nthreads, &master_thread_id, &cv, &cv_m](const tbb::blocked_range<size_t> &range) {
assert(range.begin() + 1 == range.end());
if (std::unique_lock<std::mutex> lk(cv_m); ++nthreads_running == nthreads) {
lk.unlock();
// All threads are spinning.
// Wake them up.
cv.notify_all();
} else {
// Wait for the last thread to wake the others.
cv.wait(lk, [&nthreads_running, nthreads]{return nthreads_running == nthreads;});
}
auto thread_id = std::this_thread::get_id();
if (thread_id == master_thread_id) {
// The calling thread runs the 0'th task.
assert(range.begin() == 0);
} else {
assert(range.begin() > 0);
std::ostringstream name;
name << "slic3r_tbb_" << range.begin();
set_current_thread_name(name.str().c_str());
// Set locales of the worker thread to "C".
#ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
std::setlocale(LC_ALL, "C");
#else
// We are leaking some memory here, because the newlocale() produced memory will never be released.
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
uselocale(newlocale(
#ifdef __APPLE__
LC_ALL_MASK
#else // some Unix / Linux / BSD
LC_ALL
#endif
, "C", nullptr));
#endif
}
});
}
}
+1 -8
View File
@@ -28,10 +28,6 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
area_sum_to_triangle_idx[area_sum] = t_idx;
}
if (area_sum_to_triangle_idx.empty())
// No triangle to sample from.
return {};
std::mt19937_64 mersenne_engine { 27644437 };
// random numbers on interval [0, 1)
std::uniform_real_distribution<double> fdistribution;
@@ -54,10 +50,7 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
tbb::blocked_range<size_t> r) {
for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) {
double t_sample = random_samples[s_idx].x() * area_sum;
// The keys of area_sum_to_triangle_idx are accumulated areas in double precision, while area_sum
// is a float, thus t_sample may reach or exceed the largest key and upper_bound() may return end().
auto t_it = area_sum_to_triangle_idx.upper_bound(t_sample);
size_t t_idx = (t_it == area_sum_to_triangle_idx.end() ? std::prev(t_it) : t_it)->second;
size_t t_idx = area_sum_to_triangle_idx.upper_bound(t_sample)->second;
double sq_u = std::sqrt(random_samples[s_idx].y());
double v = random_samples[s_idx].z();
+25
View File
@@ -27,6 +27,7 @@
#include <Eigen/Geometry>
#include <type_traits> // for std::enable_if_t
#include <typeinfo> // for typeid
#include <regex>
namespace Catch {
template <typename T>
@@ -321,6 +322,30 @@ TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]"
REQUIRE_FALSE(loaded);
}
TEST_CASE("A project whose components reference themselves fails to load", "[3mf][Regression]")
{
ScopedTemporaryFile temp(".3mf");
store_painted_cube(temp.string());
// Point the component back at the object that holds it. Expanding that reference used to push
// into the work list forever, growing it until the process ran out of memory.
REQUIRE(rewrite_3mf_entries(temp.string(), [](std::string& name, std::string& data) {
if (!boost::algorithm::ends_with(name, "3dmodel.model"))
return false;
std::smatch match;
if (!std::regex_search(data, match, std::regex("<object id=\"([0-9]+)\"[^>]*>\\s*<components")))
return false;
data = std::regex_replace(data, std::regex("objectid=\"[0-9]+\""), "objectid=\"" + match[1].str() + "\"");
return true;
}));
ScopedTemporaryDir backup_dir("orca_cycle_dst");
Model model;
bool loaded = true;
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
REQUIRE_FALSE(loaded);
}
TEST_CASE("A project with malformed paint data loads without the damaged facet", "[3mf][Regression]")
{
ScopedTemporaryFile temp(".3mf");