From cb7d2c698d495515dda7b2b32364ef7a065b217b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Wed, 7 Oct 2026 19:33:08 +0800 Subject: [PATCH] Cap Recursion Depth in the Plugin JSON Converters --- src/slic3r/plugin/PluginFsUtils.hpp | 23 +++++-- tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_plugin_json_depth.cpp | 70 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 tests/slic3rutils/test_plugin_json_depth.cpp diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index 7e552b7896..b7d10a9da6 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE; // Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible // values, so both go through these. -inline pybind11::object json_to_py(const nlohmann::json& j) +// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically +// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash; +// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload. +inline constexpr int kMaxJsonConversionDepth = 200; + +inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0) { namespace py = pybind11; using json = nlohmann::json; + if (depth > kMaxJsonConversionDepth) + throw py::value_error("Plugin JSON value nested too deeply"); + switch (j.type()) { case json::value_t::null: return py::none(); case json::value_t::boolean: return py::bool_(j.get()); @@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j) case json::value_t::array: { py::list lst; for (const auto& e : j) - lst.append(json_to_py(e)); + lst.append(json_to_py(e, depth + 1)); return lst; } case json::value_t::object: { py::dict d; for (auto it = j.begin(); it != j.end(); ++it) - d[py::str(it.key())] = json_to_py(it.value()); + d[py::str(it.key())] = json_to_py(it.value(), depth + 1); return d; } default: return py::none(); } } -inline nlohmann::json py_to_json(const pybind11::handle& o) +inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0) { namespace py = pybind11; using json = nlohmann::json; + if (depth > kMaxJsonConversionDepth) + throw py::value_error("Plugin value nested too deeply (possible cycle)"); + if (o.is_none()) return json(nullptr); if (py::isinstance(o)) // bool before int (bool subclasses int in Python) @@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o) if (py::isinstance(o)) { json obj = json::object(); for (auto item : py::reinterpret_borrow(o)) - obj[py::str(item.first).cast()] = py_to_json(item.second); + obj[py::str(item.first).cast()] = py_to_json(item.second, depth + 1); return obj; } if (py::isinstance(o) || py::isinstance(o)) { json arr = json::array(); for (auto e : o) - arr.push_back(py_to_json(e)); + arr.push_back(py_to_json(e, depth + 1)); return arr; } return py::str(o).cast(); // fallback: str() diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 97a282825b..2e0fcdb444 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_sort.cpp test_plugin_cloud_metadata.cpp test_plugin_audit.cpp + test_plugin_json_depth.cpp test_shortcuts.cpp test_file_url.cpp test_user_manager.cpp diff --git a/tests/slic3rutils/test_plugin_json_depth.cpp b/tests/slic3rutils/test_plugin_json_depth.cpp new file mode 100644 index 0000000000..4d993977f9 --- /dev/null +++ b/tests/slic3rutils/test_plugin_json_depth.cpp @@ -0,0 +1,70 @@ +#include +#include + +#include +#include +#include + +#include "plugin_test_utils.hpp" + +#include +#include +#include +#include +#include + +using namespace Slic3r; + +namespace { +// Brings the embedded interpreter up for one test and tears it down before boost::log does, +// mirroring the ScopedPluginManager idiom in the other plugin tests. +struct ScopedPluginManager +{ + ScopedDataDir python_data_dir{"plugin-json-depth"}; + bool initialized = PluginManager::instance().initialize(); + ~ScopedPluginManager() + { + PluginManager::instance().shutdown(); + PythonInterpreter::instance().shutdown(); + } +}; +} // namespace + +TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]") +{ + ScopedPluginManager manager; + REQUIRE(manager.initialized); + namespace py = pybind11; + py::gil_scoped_acquire gil; + + // [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough + // that the pre-fix code returns without crashing, so a regression fails cleanly rather than + // taking the process down. Built in C++ so the test does not depend on Python builtins. + py::object deep = py::int_(0); + for (int i = 0; i < 300; ++i) { + py::list wrapper; + wrapper.append(deep); + deep = std::move(wrapper); + } + CHECK_THROWS_AS(py_to_json(deep), std::exception); +} + +TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]") +{ + ScopedPluginManager manager; + REQUIRE(manager.initialized); + namespace py = pybind11; + py::gil_scoped_acquire gil; + + py::dict d; + d["a"] = py::int_(1); + py::list inner; + inner.append(py::str("x")); + inner.append(py::int_(2)); + d["b"] = inner; + + const nlohmann::json j = py_to_json(d); + CHECK(j.at("a").get() == 1); + CHECK(j.at("b").at(0).get() == "x"); + CHECK(j.at("b").at(1).get() == 2); +}