From 018c4f49ee7ac2f3cc56b49a6592a8e96f2ebbd4 Mon Sep 17 00:00:00 2001 From: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com> Date: Fri, 9 Oct 2026 06:07:03 +0200 Subject: [PATCH] Fix a crash on loading a 3MF with empty project settings (#16016) * Fix a crash on loading a 3MF with empty project settings opt_float() dereferences what option<>() returns without checking it, and option<>() is called with create = false. Three CLI sites read printable_height that way, so a 3mf whose Metadata/project_settings.config holds an empty object takes the CLI down with a null dereference. Both models shipped in resources/handy_models are such files, so `--info` on either of them segfaults. Guard the three reads the way the neighbouring reads of extruder_clearance_height_to_rod and friends already are. All three target variables are initialised to 0 and the consumer tests for > 0, so an absent setting already had a defined meaning and nothing changes for a project that carries the setting. * Add a CLI regression test for a project with empty settings Runs --info over a copy of a shipped model whose Metadata/project_settings.config has been rewritten to an empty object, so the test keeps covering the crash no matter what settings the shipped models carry later. Verified both ways: the test passes against this branch and fails with a segmentation fault against a build without the guards. --------- Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/OrcaSlicer.cpp | 8 +++- tests/cli/CMakeLists.txt | 7 +++ tests/cli/test_cli_empty_project_config.sh | 52 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100755 tests/cli/test_cli_empty_project_config.sh diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b895c25e89..4c4c001bfe 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1820,6 +1820,8 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } + // A 3mf can carry an empty project_settings.config - the models in + // resources/handy_models do - and opt_float() dereferences without checking. if (config.option("printable_height")) old_printable_height = (int)(config.opt_float("printable_height")); @@ -2505,7 +2507,8 @@ int CLI::run(int argc, char **argv) orig_printable_width = static_cast(orig_printable_bbox.size().x()); orig_printable_depth = static_cast(orig_printable_bbox.size().y()); } - orig_printable_height = (int)(config.opt_float("printable_height")); + if (config.option("printable_height")) + orig_printable_height = (int)(config.opt_float("printable_height")); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%") %__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height; if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0)) @@ -4617,7 +4620,8 @@ int CLI::run(int argc, char **argv) BoundingBoxf temp_printable_bbox(temp_printable_area); printer_plate.printable_width = static_cast(temp_printable_bbox.size().x()); printer_plate.printable_depth = static_cast(temp_printable_bbox.size().y()); - printer_plate.printable_height = (int)(config.opt_float("printable_height")); + if (config.option("printable_height")) + printer_plate.printable_height = (int)(config.opt_float("printable_height")); } if (temp_exclude_area.size() >= 4) { printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x()); diff --git a/tests/cli/CMakeLists.txt b/tests/cli/CMakeLists.txt index 171140c775..dddebee566 100644 --- a/tests/cli/CMakeLists.txt +++ b/tests/cli/CMakeLists.txt @@ -11,6 +11,13 @@ endif () add_test(NAME cli_strict_mode COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $ ${ORCA_CLI_TEST_PYTHON}) +add_test(NAME cli_empty_project_config + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $ ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR}) +set_tests_properties(cli_empty_project_config PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 300) + set_tests_properties(cli_strict_mode PROPERTIES LABELS "CLI;RequiresApp" SKIP_RETURN_CODE 77 diff --git a/tests/cli/test_cli_empty_project_config.sh b/tests/cli/test_cli_empty_project_config.sh new file mode 100755 index 0000000000..8b4dd0b480 --- /dev/null +++ b/tests/cli/test_cli_empty_project_config.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load. +# +# The CLI reads printable_height out of the project config with opt_float(), which dereferences +# what option<>() returns. With create = false that is null when the key is absent, so a project +# saved without settings used to take the CLI down with a segfault. Both models in +# resources/handy_models are such files. +# +# usage: test_cli_empty_project_config.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +SRC="${3:-}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } +[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; } + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/datadir" + +# Rewrite the project settings to an empty object, so the test holds no matter what the shipped +# models carry later on. +cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf" +"$PY" - "$WORK/empty_config.3mf" <<'PYEOF' +import shutil, sys, zipfile + +path = sys.argv[1] +entry = "Metadata/project_settings.config" +with zipfile.ZipFile(path) as src: + items = [(i, src.read(i.filename)) for i in src.infolist()] +with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst: + seen = False + for info, data in items: + if info.filename == entry: + data, seen = b"{\n}\n", True + dst.writestr(info, data) + if not seen: + dst.writestr(entry, b"{\n}\n") +shutil.move(path + ".new", path) +PYEOF + +"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1 +rc=$? +if [ $rc -ne 0 ]; then + echo "FAIL: --info on a project with empty settings exited $rc" + tail -20 "$WORK/info.txt" + exit 1 +fi +grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; } +echo "PASS: a project with empty settings loads"