From 804da5dcc72f8e37f5de261dc74ea15cd0c68ad9 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 28 Sep 2026 05:58:48 +0800 Subject: [PATCH] Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs Four CLI paths indexed vectors without checking their size and crashed with SIGSEGV on malformed input: - A project inherits_group whose length is not the filament count plus the process and printer entries was split by position. It is now ignored with a warning, as if the project had none. - An assemble list object with an empty filaments list passed validation and was then read at index 0. It is now rejected as a config error, as is a negative filament id. - --slice N --arrange 1 on a project without plate metadata read the missing plate data. It now falls back to the plate's own filaments, like the other plate data reads. - --assemble with no input model built an object with no volumes. It is now rejected as invalid parameters. A tests/cli script covers each case through the binary, since all four live inline in CLI::run(). --- src/OrcaSlicer.cpp | 26 +++- tests/cli/CMakeLists.txt | 7 + tests/cli/test_cli_malformed_input.sh | 207 ++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 tests/cli/test_cli_malformed_input.sh diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index e7facbdbd8..449dd11eec 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -699,11 +699,22 @@ static int load_assemble_plate_list(std::string config_file, std::vector>(); - if ((assemble_object.filaments.size() > 0) && (assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1)) + if (assemble_object.filaments.empty()) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments list is empty") % assemble_object.path; + return CLI_CONFIG_FILE_ERROR; + } + if ((assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1)) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count; return CLI_CONFIG_FILE_ERROR; } + // 0 keeps the default filament, as it does for --load-filament-ids. + if (std::any_of(assemble_object.filaments.begin(), assemble_object.filaments.end(), [](int id) { return id < 0; })) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1% has a negative filament id") % assemble_object.path; + return CLI_CONFIG_FILE_ERROR; + } if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) { assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get>(); @@ -1863,6 +1874,12 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("current_printer_name %1%, current_process_name %2%")%current_printer_name %current_process_name; ConfigOptionStrings* option_strings = config.option("inherits_group"); + // One entry for the process, one per filament and one for the printer. + if (option_strings && option_strings->values.size() != current_filaments_name.size() + 2) { + boost::nowide::cerr << "Warning: ignoring inherits_group with " << option_strings->values.size() << " entries, expected " + << current_filaments_name.size() + 2 << " for " << current_filaments_name.size() << " filaments" << std::endl; + option_strings = nullptr; + } if (option_strings) { current_inherits_group = option_strings->values; size_t size = current_inherits_group.size(); @@ -4733,6 +4750,11 @@ int CLI::run(int argc, char **argv) record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); flush_and_exit(CLI_INVALID_PARAMS); } + if (m_models.empty()) { + boost::nowide::cerr << "Invalid params: --assemble needs at least one input model." << std::endl; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } Model m; ModelObject* new_object = m.add_object(); new_object->name = _u8L("Assembly"); @@ -5673,7 +5695,7 @@ int CLI::run(int argc, char **argv) float w = dynamic_cast(m_print_config.option("prime_tower_width"))->value; float a = dynamic_cast(m_print_config.option("wipe_tower_rotation_angle"))->value; float v = dynamic_cast(m_print_config.option("prime_volume"))->value; - unsigned int filaments_cnt = plate_data_src[plate_to_slice-1]->slice_filaments_info.size(); + unsigned int filaments_cnt = (plate_data_src.size() >= static_cast(plate_to_slice)) ? plate_data_src[plate_to_slice-1]->slice_filaments_info.size() : 0; if ((filaments_cnt == 0) || need_skip) { // slice filaments info invalid diff --git a/tests/cli/CMakeLists.txt b/tests/cli/CMakeLists.txt index 6707159fb2..df0157b0f6 100644 --- a/tests/cli/CMakeLists.txt +++ b/tests/cli/CMakeLists.txt @@ -15,3 +15,10 @@ set_tests_properties(cli_strict_mode PROPERTIES LABELS "CLI;RequiresApp" SKIP_RETURN_CODE 77 TIMEOUT 900) + +add_test(NAME cli_malformed_input + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $ ${ORCA_CLI_TEST_PYTHON}) +set_tests_properties(cli_malformed_input PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 900) diff --git a/tests/cli/test_cli_malformed_input.sh b/tests/cli/test_cli_malformed_input.sh new file mode 100644 index 0000000000..ea96251dd6 --- /dev/null +++ b/tests/cli/test_cli_malformed_input.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# End-to-end checks that malformed CLI input fails cleanly, or loads, instead of crashing the +# orca-slicer binary. Each case lives inline in CLI::run(), so only the binary can reach it. +# +# - A project whose inherits_group does not have one entry per filament plus the process and +# printer entries is loaded as if it had none. +# - --slice N --arrange 1 on a project without plate metadata slices plate N. +# - An assemble list object with an empty filament list, or a negative filament id, is rejected +# as a config error. +# - --assemble with no input model is rejected as invalid parameters. +# +# usage: test_cli_malformed_input.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } + +# From src/libslic3r/Utils.hpp. main() returns them, so the shell sees them modulo 256. +CLI_SUCCESS=0 +CLI_INVALID_PARAMS=-2 +CLI_CONFIG_FILE_ERROR=-5 + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-malformed.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/datadir" + +# Standalone presets: without "inherits" the CLI loads them as-is, with no preset bundle. +cat > "$WORK/machine.json" <<'EOF' +{ + "type": "machine", + "from": "User", + "name": "CLI malformed input test printer", + "printable_area": ["0x0", "200x0", "200x200", "0x200"], + "printable_height": "100", + "layer_change_gcode": "G92 E0" +} +EOF +cat > "$WORK/process.json" <<'EOF' +{ + "type": "process", + "from": "User", + "name": "CLI malformed input test process" +} +EOF +cat > "$WORK/filament.json" <<'EOF' +{ + "type": "filament", + "from": "User", + "name": "CLI malformed input test filament" +} +EOF + +"$PY" - "$WORK/cube.stl" <<'EOF' +import sys + +v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)] +with open(sys.argv[1], "w") as f: + f.write("solid cube\n") + # Faces wound counter-clockwise seen from outside: -z, +z, -y, +y, -x, +x. + for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)): + for tri in ((a, b, c), (a, c, d)): + f.write("facet normal 0 0 0\nouter loop\n") + for i in tri: + f.write("vertex %g %g %g\n" % v[i]) + f.write("endloop\nendfacet\n") + f.write("endsolid cube\n") +EOF + +fails=0 +fail() { echo "FAIL: $*"; fails=$((fails + 1)); } + +# run [option...]: run into $WORK/, keeping the log and the shell status there. +run() { + local out="$WORK/$1"; shift + mkdir -p "$out" + timeout 300 "$BIN" --datadir "$WORK/datadir" --outputdir "$out" "$@" > "$out/log" 2>&1 + echo $? > "$out/status" +} + +# run_presets [option...]: run with the standalone presets loaded. +run_presets() { + local tag="$1"; shift + run "$tag" --load-settings "$WORK/machine.json;$WORK/process.json" --load-filaments "$WORK/filament.json" "$@" +} + +# expect_status +expect_status() { + local got; got="$(cat "$WORK/$1/status")" + [ "$got" -eq $(( $2 & 255 )) ] || fail "$1: shell status $got, want $(( $2 & 255 )) (code $2)" +} + +# expect_result : a failing run must also carry an error_string. +expect_result() { + "$PY" - "$WORK/$1/result.json" "$2" <<'EOF' || fail "$1: result.json" +import json, sys + +try: + with open(sys.argv[1]) as f: + result = json.load(f) +except (OSError, ValueError) as e: + sys.exit("cannot read %s: %s" % (sys.argv[1], e)) +want_rc = int(sys.argv[2]) +if result.get("return_code") != want_rc: + sys.exit("return_code %r, want %d" % (result.get("return_code"), want_rc)) +if want_rc != 0 and not result.get("error_string"): + sys.exit("no error_string") +EOF +} + +# expect_log +expect_log() { + grep -qF -- "$2" "$WORK/$1/log" || fail "$1: log does not mention \"$2\"" +} + +# expect_gcode +expect_gcode() { + compgen -G "$WORK/$1/*.gcode" > /dev/null || fail "$1: no G-code was exported" +} + +# rewrite_3mf inherits | no-plates +rewrite_3mf() { + "$PY" - "$@" <<'EOF' +import json, re, sys, zipfile + +src, dst, mode = sys.argv[1:4] +with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout: + for info in zin.infolist(): + data = zin.read(info.filename) + if mode == "inherits" and info.filename == "Metadata/project_settings.config": + config = json.loads(data) + config["inherits_group"] = json.loads(sys.argv[4]) + data = json.dumps(config, indent=4).encode() + elif mode == "no-plates": + if re.match(r"Metadata/plate_\d+\.", info.filename): + continue + if info.filename == "Metadata/model_settings.config": + data = re.sub(rb"\s*.*?", b"", data, flags=re.S) + zout.writestr(info, data) +EOF +} + +# assemble_list +assemble_list() { + cat > "$1" <