Compare commits

..
5 changed files with 14 additions and 91 deletions
+7
View File
@@ -115,6 +115,9 @@ static const std::unordered_map<std::string, AuditEventCategory> audit_event_cat
{"subprocess.Popen", AuditEventCategory::ProcessCreate}, {"subprocess.Popen", AuditEventCategory::ProcessCreate},
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate}, {"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate}, {"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
// processreplace: exec* replaces the current process image rather than spawning a child
{"os.exec", AuditEventCategory::ProcessReplace},
}; };
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited. // Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
@@ -708,6 +711,7 @@ static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_targ
{"pty.spawn", {0}}, {"pty.spawn", {0}},
{"_winapi.CreateProcess", {1, 0}}, {"_winapi.CreateProcess", {1, 0}},
{"_posixsubprocess.fork_exec", {0}}, {"_posixsubprocess.fork_exec", {0}},
{"os.exec", {0}},
}; };
AuditEventCategory open_category(PyObject* args) AuditEventCategory open_category(PyObject* args)
@@ -761,6 +765,7 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, Plugi
case AuditEventCategory::Http: return &permissions.network_http; case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket; case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process; case AuditEventCategory::ProcessCreate: return &permissions.process;
case AuditEventCategory::ProcessReplace: return &permissions.process;
default: return nullptr; default: return nullptr;
} }
} }
@@ -832,6 +837,8 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name,
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list); return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate: case AuditEventCategory::ProcessCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list); return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessReplace:
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\n%s"), plugin_name, target_list);
default: default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name); return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
} }
+1
View File
@@ -45,6 +45,7 @@ enum class AuditEventCategory {
Http, Http,
Socket, Socket,
ProcessCreate, ProcessCreate,
ProcessReplace,
Threading, Threading,
}; };
+6 -17
View File
@@ -25,19 +25,11 @@ extern const char* const INSTALL_STATE_FILE;
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible // Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
// values, so both go through these. // values, so both go through these.
// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically inline pybind11::object json_to_py(const nlohmann::json& j)
// 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; namespace py = pybind11;
using json = nlohmann::json; using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin JSON value nested too deeply");
switch (j.type()) { switch (j.type()) {
case json::value_t::null: return py::none(); case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>()); case json::value_t::boolean: return py::bool_(j.get<bool>());
@@ -48,27 +40,24 @@ inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0)
case json::value_t::array: { case json::value_t::array: {
py::list lst; py::list lst;
for (const auto& e : j) for (const auto& e : j)
lst.append(json_to_py(e, depth + 1)); lst.append(json_to_py(e));
return lst; return lst;
} }
case json::value_t::object: { case json::value_t::object: {
py::dict d; py::dict d;
for (auto it = j.begin(); it != j.end(); ++it) for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value(), depth + 1); d[py::str(it.key())] = json_to_py(it.value());
return d; return d;
} }
default: return py::none(); default: return py::none();
} }
} }
inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0) inline nlohmann::json py_to_json(const pybind11::handle& o)
{ {
namespace py = pybind11; namespace py = pybind11;
using json = nlohmann::json; using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin value nested too deeply (possible cycle)");
if (o.is_none()) if (o.is_none())
return json(nullptr); return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python) if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
@@ -84,13 +73,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0)
if (py::isinstance<py::dict>(o)) { if (py::isinstance<py::dict>(o)) {
json obj = json::object(); json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o)) for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second, depth + 1); obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
return obj; return obj;
} }
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) { if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array(); json arr = json::array();
for (auto e : o) for (auto e : o)
arr.push_back(py_to_json(e, depth + 1)); arr.push_back(py_to_json(e));
return arr; return arr;
} }
return py::str(o).cast<std::string>(); // fallback: str() return py::str(o).cast<std::string>(); // fallback: str()
-1
View File
@@ -34,7 +34,6 @@ add_executable(${_TEST_NAME}_tests
test_plugin_sort.cpp test_plugin_sort.cpp
test_plugin_cloud_metadata.cpp test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp test_plugin_audit.cpp
test_plugin_json_depth.cpp
test_shortcuts.cpp test_shortcuts.cpp
test_file_url.cpp test_file_url.cpp
test_user_manager.cpp test_user_manager.cpp
@@ -1,73 +0,0 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include "plugin_test_utils.hpp"
#include <cstdint>
#include <exception>
#include <utility>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
#include <pybind11/gil.h>
#include <pybind11/pytypes.h>
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<std::int64_t>() == 1);
CHECK(j.at("b").at(0).get<std::string>() == "x");
CHECK(j.at("b").at(1).get<std::int64_t>() == 2);
}