From bd1304443cb417d39c7be7d4182d8d8c1f737908 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Tue, 15 Sep 2026 07:03:20 -0500 Subject: [PATCH] fix: guard per-filament array reads against short config arrays (#14789) * fix: guard H2C per-filament array reads against short config arrays The H2C tool-ordering, wipe-tower, and g-code export paths index per-filament config arrays by filament/tool id. A config with fewer entries than the filament count (partial or legacy projects, minimal test configs) makes these reads run past the end of the vector: silent under a normal STL, but UB that aborts under the flatpak build's bounds-checked STL (_GLIBCXX_ASSERTIONS). Route the reads through the existing clamping accessors (get_at, get_filament_category, is_in_same_extruder) and add a small clamp helper for filament_change_length. The guards are no-ops when the arrays are sized to the filament count, so correctly specified configs are unaffected. * fix: size the grouping context's filament_info to the filament count build_filament_group_context built model_info.filament_info by walking filament_type, so a config whose filament_type is shorter than the filament count produced a short vector. FilamentGroup indexes filament_info by filament id, so clamping the individual reads only moved the out-of-bounds access downstream. Loop to filament_nums and read all three fields through get_at, and drop filament_ids entries past the filament count, since the grouping code pairs filament_ids and filament_info by position. Adds a regression test with four filaments and one-entry filament_type / filament_is_support. Without the fix it throws bad_alloc from copying a garbage std::string read past the end. * fix: guard the carousel nozzle-change length reads too The carousel branch added in b90ac13d86/b0dddb4648 reads m_filaments_change_length by tool id without a bounds check, the same pattern this branch already routed through filament_change_length_at a few lines above in both plan_toolchange and plan_tower_new. * fix: guard WipeTower per-filament array reads against short config arrays The BambuStudio WipeTower sync reintroduced raw per-filament array indexing that reads out of bounds when a config leaves an array shorter than the filament count: m_physical_extruder_map in format_line_M104/M109 (indexed even when empty), and m_filament_categories in get_wall_skip_points and get_wall_filament_for_all_layer. Silent on a normal STL, a hard abort under the bounds-checked STL the Flatpak build uses. Bounds-check the physical extruder map before indexing (omitting the T token, as the existing -1 path already does), and route the two raw m_filament_categories reads through the clamping get_filament_category() accessor the surrounding code already uses. No change for correctly-sized configs. --- src/libslic3r/GCode.cpp | 4 +- src/libslic3r/GCode/ToolOrdering.cpp | 19 ++++---- src/libslic3r/GCode/WipeTower.cpp | 8 ++-- .../test_toolordering_nozzle_group.cpp | 44 +++++++++++++++++++ 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4aa45a60ed..902786bf7d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3555,7 +3555,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato auto used_filaments = print.get_slice_used_filaments(false); this->placeholder_parser().set("is_all_bbl_filament", std::all_of(used_filaments.begin(), used_filaments.end(), [&](auto idx) { - return m_config.filament_vendor.values[idx] == "Bambu Lab"; + return m_config.filament_vendor.get_at(idx) == "Bambu Lab"; })); //add during_print_exhaust_fan_speed @@ -3572,7 +3572,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("outer_wall_volumetric_speed", new ConfigOptionFloat(outer_wall_volumetric_speed)); auto first_layer_filaments = print.get_slice_used_filaments(true); - bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.values[idx] == "TPU"; }); + bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.get_at(idx) == "TPU"; }); this->placeholder_parser().set("has_tpu_in_first_layer", new ConfigOptionBool(has_tpu_in_first_layer)); if (print.calib_params().mode == CalibMode::Calib_PA_Line) { diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index c6517c6e65..e9be0171e4 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1488,10 +1488,10 @@ static FilamentGroupContext build_filament_group_context( auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament); - std::vector filament_types = print_config.filament_type.values; - std::vector filament_colours = print_config.filament_colour.values; - std::vector filament_is_support = print_config.filament_is_support.values; - std::vector filament_ids = print_config.filament_ids.values; + // The grouping code walks filament_ids and indexes filament_info by the same position. + std::vector filament_ids = print_config.filament_ids.values; + if (filament_ids.size() > filament_nums) + filament_ids.resize(filament_nums); FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode : FGMode::FlushMode; context.model_info.flush_matrix = std::move(nozzle_flush_mtx); @@ -1500,11 +1500,14 @@ static FilamentGroupContext build_filament_group_context( context.model_info.filament_ids = filament_ids; context.model_info.unprintable_volumes = unprintable_volumes; - for (size_t idx = 0; idx < filament_types.size(); ++idx) { + // Consumers index filament_info by filament id, so it must span the filament count: a partial + // or legacy config can leave any of these arrays short, and get_at clamps. + context.model_info.filament_info.reserve(filament_nums); + for (size_t idx = 0; idx < filament_nums; ++idx) { FilamentGroupUtils::FilamentInfo info; - info.color = filament_colours[idx]; - info.type = filament_types[idx]; - info.is_support = filament_is_support[idx]; + info.color = print_config.filament_colour.get_at(idx); + info.type = print_config.filament_type.get_at(idx); + info.is_support = print_config.filament_is_support.get_at(idx); context.model_info.filament_info.emplace_back(std::move(info)); } diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 589ac14bad..e80433f4ae 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -1349,7 +1349,7 @@ public: // flavor it reaches understands, not the zero dwell the other flavors flush with. buffer += "M400\n"; buffer += "M104"; - if (target_extruder != -1) + if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size())) buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer if (!comment.empty()) buffer += " ;" + comment; @@ -1361,7 +1361,7 @@ public: WipeTowerWriter &format_line_M109(int target_temp, int target_extruder, const std::string &comment = std::string()) { std::string buffer = "M109"; - if (target_extruder != -1) + if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size())) buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer if (!comment.empty()) buffer += " ;" + comment; @@ -3309,7 +3309,7 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer, int layer_id) if (!cur_block_depth.count(m_filpar[new_filament].category)) cur_block_depth[m_filpar[new_filament].category] = block->start_depth; process_depth = cur_block_depth[m_filpar[new_filament].category]; if (is_need_ramming(new_filament, old_filament, layer_id)) { - if (m_filament_categories[new_filament] == m_filament_categories[old_filament]) + if (get_filament_category(new_filament) == get_filament_category(old_filament)) process_depth += nozzle_change_depth; else { if (!cur_block_depth.count(m_filpar[old_filament].category)) { @@ -4783,7 +4783,7 @@ int WipeTower::get_wall_filament_for_all_layer() int filament_id = -1; int filament_count = 0; for (auto iter = filament_counts.begin(); iter != filament_counts.end(); ++iter) { - if (m_filament_categories[iter->first] == selected_category && iter->second > filament_count) { + if (get_filament_category(iter->first) == selected_category && iter->second > filament_count) { filament_id = iter->first; filament_count = iter->second; } diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index 26e36c0dbf..d01ccf5856 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -163,6 +163,50 @@ TEST_CASE("H2C multi-nozzle: filaments get distinct nozzles on the 6-nozzle extr } } +TEST_CASE("Grouping context spans the filament count with mis-sized config arrays", "[ToolOrdering][H2C]") +{ + // FilamentGroup indexes the grouping context's filament_info by filament id, so a short + // per-filament array must not shorten it: the reads run off the end. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // Single 6-nozzle extruder: opens the grouping engine without needing a BBL multi-extruder. + config.option("nozzle_diameter", true)->values = {0.4}; + config.option("extruder_max_nozzle_count", true)->values = {6}; + config.option("extruder_nozzle_stats", true)->values = {"Standard#6"}; + + // Four filaments, with filament_type / filament_is_support left short on purpose. + config.option("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00"}; + config.option("filament_type", true)->values = {"PLA"}; + config.option("filament_is_support", true)->values = {0}; + config.option("filament_diameter", true)->values = {1.75, 1.75, 1.75, 1.75}; + config.option("filament_map", true)->values = {1, 1, 1, 1}; + config.option("flush_volumes_matrix", true)->values = std::vector(16, 140.); + config.option("flush_multiplier", true)->values = {1.}; + + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); + + Print print; + print.apply(model, config); + // apply() does not pad the per-filament arrays, so the mis-sizing survives into the engine. + REQUIRE(print.config().filament_type.values.size() < print.config().filament_colour.values.size()); + + std::vector> layer_filaments = {{0, 1}, {1, 2}, {2, 3}}; + + SECTION("short per-filament arrays still yield one entry per filament") { + auto result = ToolOrdering::get_recommended_filament_maps(layer_filaments, &print, FilamentMapMode::fmmAutoForFlush, {}, {}); + REQUIRE(result.get_extruder_map(false).size() == 4); + for (int f = 0; f < 4; ++f) + REQUIRE(result.get_extruder_id(f) == 0); + } + + SECTION("filament_ids longer than the filament count is truncated, not paired past the end") { + config.option("filament_ids", true)->values = {"a", "b", "c", "d", "e", "f"}; + print.apply(model, config); + auto result = ToolOrdering::get_recommended_filament_maps(layer_filaments, &print, FilamentMapMode::fmmAutoForFlush, {}, {}); + REQUIRE(result.get_extruder_map(false).size() == 4); + } +} + TEST_CASE("H2C dynamic selector: per-layer nozzle ids reach the g-code surface", "[ToolOrdering][H2C][Dynamic]") { // The per-layer regroup engine