From 67a16dabca4af8b0de871d75bea08a7ce2cfc9f8 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 8 Oct 2026 23:49:33 +0800 Subject: [PATCH] fix: prompt for permission when plugin tries to create a thread (#16248) * fix: prompt for permission when plugin tries to create a thread * fix: request permission on main thread --- src/slic3r/plugin/PluginAuditManager.cpp | 82 ++++++++++++++++++++--- src/slic3r/plugin/PluginFsUtils.cpp | 2 + src/slic3r/plugin/PluginFsUtils.hpp | 1 + tests/slic3rutils/test_plugin_install.cpp | 2 + 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index f3bd6c1636..efb730da8c 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -116,6 +116,8 @@ static const std::unordered_map audit_event_cat {"_winapi.CreateProcess", AuditEventCategory::ProcessCreate}, {"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate}, + // threading + {"_thread.start_new_thread", AuditEventCategory::Threading}, // processreplace: exec* replaces the current process image rather than spawning a child {"os.exec", AuditEventCategory::ProcessReplace}, }; @@ -739,6 +741,12 @@ std::vector audit_targets(const std::string& event_name, AuditEvent } return targets; } + case AuditEventCategory::Threading: + // Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant + // persists per plugin: the permission list matches targets by exact string, and the + // started function's repr embeds an address that changes every run. + targets.emplace_back("thread"); + return targets; default: break; } @@ -765,6 +773,7 @@ std::vector* permission_list_for(AuditEventCategory category, Plugi case AuditEventCategory::Http: return &permissions.network_http; case AuditEventCategory::Socket: return &permissions.network_socket; case AuditEventCategory::ProcessCreate: return &permissions.process; + case AuditEventCategory::Threading: return &permissions.threading; case AuditEventCategory::ProcessReplace: return &permissions.process; default: return nullptr; } @@ -837,6 +846,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); case AuditEventCategory::ProcessCreate: return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::Threading: + return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name); case AuditEventCategory::ProcessReplace: return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\n%s"), plugin_name, target_list); default: @@ -844,6 +855,67 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name, } } +// Builds and shows the modal permission prompt. Must run on the GUI thread. +bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name, + const std::vector& unresolved) +{ + wxString target_list; + for (const auto& target : unresolved) + target_list += wxString::FromUTF8(target.c_str()) + "\n"; + + wxMessageDialog dialog(nullptr, + audit_message(category, wxString::FromUTF8(plugin_name.c_str()), + wxString::FromUTF8(event_name.c_str()), target_list), + _L("Plugin permission request"), wxYES_NO | wxICON_WARNING); + return dialog.ShowModal() == wxID_YES; +} + +// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state +// freshly because the async prompt outlives the caller's stack copy of it. +void persist_grant(const std::string& plugin_key, AuditEventCategory category, const std::vector& targets) +{ + PluginInstallState state; + if (!PluginManager::instance().get_install_state(plugin_key, state)) + return; + + std::vector* permission_list = permission_list_for(category, state.permissions); + if (!permission_list) + return; + + for (const auto& target : targets) + persist_permission(plugin_key, state, *permission_list, target); +} + +// Requests permission for an audited event, returning true when it is already granted or the user +// approves an inline prompt. +// +// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the +// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and +// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops. +// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that +// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the +// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt +// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered +// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without +// re-prompting. +bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name, + const std::string& event_name, const std::vector& unresolved) +{ + if (wxTheApp == nullptr || GUI::wxGetApp().is_closing()) + return false; + + if (wxIsMainThread()) + return prompt_for_targets(category, plugin_name, event_name, unresolved); + + GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() { + if (wxTheApp == nullptr || GUI::wxGetApp().is_closing()) + return; + if (prompt_for_targets(category, plugin_name, event_name, unresolved)) + persist_grant(plugin_key, category, unresolved); + }); + return false; +} + int decide_audited_event(PluginAuditManager& mgr, PluginInstallState& state, const std::string& plugin_key, @@ -864,15 +936,7 @@ int decide_audited_event(PluginAuditManager& mgr, return 0; } - wxString target_list; - for (const auto& target : unresolved) - target_list += wxString::FromUTF8(target.c_str()) + "\n"; - - wxMessageDialog dialog(nullptr, - audit_message(category, wxString::FromUTF8(plugin_name.c_str()), - wxString::FromUTF8(event_name.c_str()), target_list), - _L("Plugin permission request"), wxYES_NO | wxICON_WARNING); - if (dialog.ShowModal() != wxID_YES) + if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved)) return report_denied(mgr, event_name, {false, "audit permission required"}); if (permission_list) diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp index e3b1bc3bee..7956d01bb0 100644 --- a/src/slic3r/plugin/PluginFsUtils.cpp +++ b/src/slic3r/plugin/PluginFsUtils.cpp @@ -807,6 +807,7 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall read_string_list("network_http", parsed.permissions.network_http); read_string_list("network_socket", parsed.permissions.network_socket); read_string_list("process", parsed.permissions.process); + read_string_list("threading", parsed.permissions.threading); } if (state.contains("enabled") && state["enabled"].is_boolean()) @@ -850,6 +851,7 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin {"network_http", state.permissions.network_http}, {"network_socket", state.permissions.network_socket}, {"process", state.permissions.process}, + {"threading", state.permissions.threading}, }; nlohmann::json capabilities = nlohmann::json::array(); diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index 7e552b7896..bd335373d0 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -92,6 +92,7 @@ struct PluginPermissions std::vector network_http; std::vector network_socket; std::vector process; + std::vector threading; }; struct PluginInstallState { diff --git a/tests/slic3rutils/test_plugin_install.cpp b/tests/slic3rutils/test_plugin_install.cpp index d79519c236..b860486979 100644 --- a/tests/slic3rutils/test_plugin_install.cpp +++ b/tests/slic3rutils/test_plugin_install.cpp @@ -122,6 +122,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins state.permissions.network_http = {"https://api.example.com"}; state.permissions.network_socket = {"192.168.45.6:443"}; state.permissions.process = {"/usr/bin/curl"}; + state.permissions.threading = {"thread"}; REQUIRE(write_install_state(plugin_dir, state)); // Permission data is persisted in the same sidecar as the installation metadata. @@ -132,6 +133,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins CHECK(persisted.permissions.network_http == state.permissions.network_http); CHECK(persisted.permissions.network_socket == state.permissions.network_socket); CHECK(persisted.permissions.process == state.permissions.process); + CHECK(persisted.permissions.threading == state.permissions.threading); // Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still // 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare