From 73059233f6d792700a90d7c6180b30cf29942fd2 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Tue, 20 Oct 2026 03:26:10 +0800 Subject: [PATCH] fix: Allow plugins to read the embedded Python stdlib --- src/slic3r/plugin/PluginAuditManager.cpp | 46 +++++++++++++++++++++++- src/slic3r/plugin/PluginAuditManager.hpp | 5 +++ tests/slic3rutils/test_plugin_audit.cpp | 34 ++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index 9cf010d803..6ad444e54f 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -498,6 +498,12 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat candidate = absolute_candidate; } + // Importing Python modules reads their source and may inspect package directories. These + // reads are normal interpreter work, not plugin data access; keep the exemption read-only + // and exclude third-party packages from the stdlib roots. + if (!is_write && is_stdlib_path(candidate)) + return {true, ""}; + // A root that doesn't allow writes only matches a read-shaped request; a write/create/ // delete-shaped one falls through to "outside allowed root" for that root even though the // path is physically inside it. @@ -1015,6 +1021,22 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, b static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing; +static const char* audit_category_name(AuditEventCategory category) +{ + switch (category) { + case AuditEventCategory::FsRead: return "FsRead"; + case AuditEventCategory::FsReadWrite: return "FsReadWrite"; + case AuditEventCategory::FsCreate: return "FsCreate"; + case AuditEventCategory::FsDelete: return "FsDelete"; + case AuditEventCategory::Http: return "Http"; + case AuditEventCategory::Socket: return "Socket"; + case AuditEventCategory::ProcessCreate: return "ProcessCreate"; + case AuditEventCategory::ProcessReplace: return "ProcessReplace"; + case AuditEventCategory::Threading: return "Threading"; + default: return "None"; + } +} + // Asks the user to grant an audited event, returning true when they approve an inline prompt. // `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has // no target (by_event), which the prompt does not show as a target. @@ -1032,6 +1054,16 @@ bool request_permission(AuditEventCategory category, const std::string& plugin_k const std::string& event_name, bool by_event, const std::vector& keys) { const std::vector shown = by_event ? std::vector{} : keys; + std::string targets_text = by_event ? "" : ""; + for (const auto& target : shown) { + if (!targets_text.empty()) + targets_text += ", "; + targets_text += target; + } + + BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name + << " category=" << audit_category_name(category) << " targets=[" << targets_text << "]" + << (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{}); if (s_permission_prompt_for_testing) return s_permission_prompt_for_testing(event_name, shown); @@ -1198,6 +1230,18 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename) return it->second; } +bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path) +{ + if (path.empty()) + return false; + + const auto inside_any = [&path](const std::vector& dirs) { + return std::any_of(dirs.begin(), dirs.end(), + [&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); }); + }; + return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs); +} + void PluginAuditManager::release_frames(std::vector& frames) { if (frames.empty()) @@ -1266,7 +1310,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user void PluginAuditManager::install_hook() { - // Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame). + // Identifies stdlib frames for operation roots and stdlib paths for read-only access. PluginAuditDetail::collect_stdlib_dirs(); // data_dir() is the primary globally-allowed root during plugin execution: read+write. It diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp index ac6d99a4da..af4172d7f1 100644 --- a/src/slic3r/plugin/PluginAuditManager.hpp +++ b/src/slic3r/plugin/PluginAuditManager.hpp @@ -173,6 +173,11 @@ private: static int audit_hook(const char* event, PyObject* args, void* user_data); + // True for paths inside the interpreter's stdlib, excluding site-packages. Unlike + // is_stdlib_file(), this query is not cached because filesystem paths may resolve through + // symlinks that can change. + static bool is_stdlib_path(const boost::filesystem::path& path); + // Drops the references in `frames`, keeping the caller's error and denial state intact. void release_frames(std::vector& frames); diff --git a/tests/slic3rutils/test_plugin_audit.cpp b/tests/slic3rutils/test_plugin_audit.cpp index debf5784b1..b2d8f9cf8f 100644 --- a/tests/slic3rutils/test_plugin_audit.cpp +++ b/tests/slic3rutils/test_plugin_audit.cpp @@ -1119,3 +1119,37 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][ CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string())); CHECK_FALSE(PluginAuditManager::is_stdlib_file("")); } + +TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]") +{ + AuditHarness harness; + if (!harness.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + harness.add_plugin(PLUGIN); + py::gil_scoped_acquire gil; + PythonSide python(harness); + + const py::dict paths = py::module_::import("sysconfig").attr("get_paths")(); + const fs::path stdlib = py::str(paths["stdlib"]).cast(); + const fs::path purelib = py::str(paths["purelib"]).cast(); + // os.py ships with every CPython installation. A site-packages path is third-party code even + // when the directory is empty: the prompt must come before the missing file is noticed. + const fs::path stdlib_file = stdlib / "os.py"; + REQUIRE(fs::exists(stdlib_file)); + + python.globals["STDLIB_FILE"] = stdlib_file.string(); + python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string(); + python.run_as(PLUGIN, R"PY( +read_outcome = attempt(lambda: open(STDLIB_FILE).read()) +write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close()) +site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close()) +)PY"); + + CHECK(python.str("read_outcome") == "ok"); + CHECK(python.str("write_outcome") == "denied"); + CHECK(python.str("site_packages_outcome") == "denied"); + // Only the write and the site-packages read are asked about; the stdlib read is implicit. + const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n"; + const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n"; + CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt); +}