Compare commits

..

1 Commits

Author SHA1 Message Date
Ian Chua
277ff35325 feat: add option to use mold to build deps on linux 2026-08-31 14:50:24 +08:00
24 changed files with 277 additions and 1240 deletions

View File

@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null
function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L][-M]"
echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)"
echo " -b: build in Debug mode"
@@ -28,6 +28,7 @@ function usage() {
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)"
echo " -M: use mold as linker (if available)"
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
@@ -41,7 +42,7 @@ unset name
BUILD_DIR=build
BUILD_CONFIG=Release
FORWARDED_ARGS=()
while getopts ":1j:bcCdDeFghiprstulL" opt ; do
while getopts ":1j:bcCdDeFghiprstulLM" opt ; do
case ${opt} in
1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -118,6 +119,10 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
USE_LLD="1"
FORWARDED_ARGS+=("-L")
;;
M )
USE_MOLD="1"
FORWARDED_ARGS+=("-M")
;;
* )
echo "Unknown argument '${opt}', aborting."
exit 1
@@ -135,6 +140,11 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1
fi
if [[ -n "${USE_LLD}" ]] && [[ -n "${USE_MOLD}" ]] ; then
echo "Error: -L and -M are mutually exclusive."
exit 1
fi
function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10
@@ -504,6 +514,18 @@ if [[ -n "${USE_LLD}" ]] ; then
fi
fi
# Configure use of mold as the linker when requested
export CMAKE_MOLD_LINKER_ARGS=()
if [[ -n "${USE_MOLD}" ]] ; then
if command -v mold >/dev/null 2>&1 ; then
MOLD_BIN=$(command -v mold)
export CMAKE_MOLD_LINKER_ARGS=(-DCMAKE_LINKER="${MOLD_BIN}" -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=mold)
else
echo "Error: mold not found. Please install the 'mold' package or omit -M."
exit 1
fi
fi
export CMAKE_CCACHE_ARGS=()
CMAKE_CCACHE=${CMAKE_CCACHE:-}
if [ -n "$CMAKE_CCACHE" ]; then
@@ -536,7 +558,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1
fi
@@ -556,7 +578,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}")
fi
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \

View File

@@ -16,27 +16,24 @@ namespace Slic3r
// This block is never executed at runtime.
static void _toolhead_translation_markers()
{
// Possible runtime values of tool_head_display_names, marked for extraction.
static const char *const markers[] = {
L("Main Extruder"), L("Main extruder"), L("main extruder"),
L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"),
L("Left Extruder"), L("Left extruder"), L("left extruder"),
L("Right Extruder"), L("Right extruder"), L("right extruder"),
L("Main Nozzle"), L("Main nozzle"), L("main nozzle"),
L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"),
L("Left Nozzle"), L("Left nozzle"), L("left nozzle"),
L("Right Nozzle"), L("Right nozzle"), L("right nozzle"),
L("Main Hotend"), L("Main hotend"), L("main hotend"),
L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"),
L("Left Hotend"), L("Left hotend"), L("left hotend"),
L("Right Hotend"), L("Right hotend"), L("right hotend"),
// standalone position words (short_name=true runtime results)
L("main"), L("auxiliary"),
L("Main"), L("Auxiliary"),
L("left"), L("right"),
L("Left"), L("Right"),
};
(void) markers;
// Dynamic toolhead display names from JSON config — xgettext cannot scan these
L("Main Extruder"); L("Main extruder"); L("main extruder");
L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder");
L("Left Extruder"); L("Left extruder"); L("left extruder");
L("Right Extruder"); L("Right extruder"); L("right extruder");
L("Main Nozzle"); L("Main nozzle"); L("main nozzle");
L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle");
L("Left Nozzle"); L("Left nozzle"); L("left nozzle");
L("Right Nozzle"); L("Right nozzle"); L("right nozzle");
L("Main Hotend"); L("Main hotend"); L("main hotend");
L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend");
L("Left Hotend"); L("Left hotend"); L("left hotend");
L("Right Hotend"); L("Right hotend"); L("right hotend");
// standalone position words (short_name=true runtime results)
L("main"); L("auxiliary");
L("Main"); L("Auxiliary");
L("left"); L("right");
L("Left"); L("Right");
}
std::string DevPrinterConfigUtil::m_resource_file_path = "";

View File

@@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt)
}
auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject());
if (auto ptr = m_nozzle_rack.lock(); item && ptr) {
if (item; auto ptr = m_nozzle_rack.lock()) {
int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r);
if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) {
SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id));

View File

@@ -862,6 +862,7 @@ void MachineInfoPanel::update_ams_ext(MachineObject *obj)
if (new_extra_ams_ver != obj->new_ver_list.end())
has_new_version = true;
extra_ams_it->second.sw_new_ver;
if (has_new_version) {
m_extra_ams_panel->m_ams_new_version_img->Show();
ver_text = new_extra_ams_ver->second.sw_ver;

View File

@@ -114,6 +114,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL;
info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND;
wxColour(255, 255, 255);
}
if (it->second->is_tray_info_ready() && obj->cali_version >= 0) {
@@ -170,6 +171,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
info.filament_id = "";
info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL;
wxColour(255, 255, 255);
}
info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL;
if (tray.is_tray_info_ready() && obj->cali_version >= 0) {

View File

@@ -1014,12 +1014,8 @@ void FanControlPopupNew::init_names(MachineObject* obj) {
if (obj) {
const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text");
if (!special_cooling_text.empty()) {
// Possible runtime values of special_cooling_text, marked for extraction.
static const char *const markers[] = {
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."),
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."),
};
(void) markers;
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air.");
label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text);
}
}
@@ -1032,13 +1028,9 @@ wxString FanControlPopupNew::get_fan_func_name(int mode, int submode, AIR_FUN fu
const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode);
if (!func_text.empty())
{
// Possible runtime values of func_text, marked for extraction.
static const char *const markers[] = {
L_CONTEXT("Right(Aux)", "air_duct"),
L_CONTEXT("Right(Filter)", "air_duct"),
L_CONTEXT("Left(Aux)", "air_duct"),
};
(void) markers;
L_CONTEXT("Right(Aux)", "air_duct");
L_CONTEXT("Right(Filter)", "air_duct");
L_CONTEXT("Left(Aux)", "air_duct");
return _L_CONTEXT(func_text, "air_duct");
}
}

View File

@@ -7,127 +7,11 @@
#include <boost/algorithm/string/predicate.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <future>
#include <slic3r/GUI/BindDialog.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <wx/event.h>
#include <wx/msgdlg.h>
namespace Slic3r {
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
static const std::unordered_map<std::string, AuditEventCategory> audit_event_categories{
// fsread
{"glob.glob", AuditEventCategory::FsRead},
{"glob.glob/2", AuditEventCategory::FsRead},
{"os.fwalk", AuditEventCategory::FsRead},
{"os.getxattr", AuditEventCategory::FsRead},
{"os.listdir", AuditEventCategory::FsRead},
{"os.listdrives", AuditEventCategory::FsRead},
{"os.listmounts", AuditEventCategory::FsRead},
{"os.listvolumes", AuditEventCategory::FsRead},
{"os.listxattr", AuditEventCategory::FsRead},
{"os.scandir", AuditEventCategory::FsRead},
{"os.walk", AuditEventCategory::FsRead},
{"pathlib.Path.glob", AuditEventCategory::FsRead},
{"pathlib.Path.rglob", AuditEventCategory::FsRead},
// fsreadwrite
{"os.chflags", AuditEventCategory::FsReadWrite},
{"os.chmod", AuditEventCategory::FsReadWrite},
{"os.chown", AuditEventCategory::FsReadWrite},
{"os.removexattr", AuditEventCategory::FsReadWrite},
{"os.rename", AuditEventCategory::FsReadWrite},
{"os.setxattr", AuditEventCategory::FsReadWrite},
{"os.truncate", AuditEventCategory::FsReadWrite},
{"os.utime", AuditEventCategory::FsReadWrite},
{"shutil.chown", AuditEventCategory::FsReadWrite},
{"shutil.copymode", AuditEventCategory::FsReadWrite},
{"shutil.copystat", AuditEventCategory::FsReadWrite},
{"shutil.copyfile", AuditEventCategory::FsReadWrite},
{"shutil.copytree", AuditEventCategory::FsReadWrite},
{"shutil.make_archive", AuditEventCategory::FsReadWrite},
{"shutil.move", AuditEventCategory::FsReadWrite},
{"shutil.unpack_archive", AuditEventCategory::FsReadWrite},
// fscreate
{"os.link", AuditEventCategory::FsCreate},
{"os.mkdir", AuditEventCategory::FsCreate},
{"os.symlink", AuditEventCategory::FsCreate},
{"tempfile.mkdtemp", AuditEventCategory::FsCreate},
{"tempfile.mkstemp", AuditEventCategory::FsCreate},
{"_winapi.CreateJunction", AuditEventCategory::FsCreate},
// fsdelete
{"os.remove", AuditEventCategory::FsDelete},
{"os.rmdir", AuditEventCategory::FsDelete},
{"shutil.rmtree", AuditEventCategory::FsDelete},
// http
{"http.client.connect", AuditEventCategory::Http},
{"http.client.send", AuditEventCategory::Http},
{"urllib.Request", AuditEventCategory::Http},
// socket
{"socket.__new__", AuditEventCategory::Socket},
{"socket.bind", AuditEventCategory::Socket},
{"socket.connect", AuditEventCategory::Socket},
{"socket.getaddrinfo", AuditEventCategory::Socket},
{"socket.gethostbyaddr", AuditEventCategory::Socket},
{"socket.gethostbyname", AuditEventCategory::Socket},
{"socket.gethostname", AuditEventCategory::Socket},
{"socket.getnameinfo", AuditEventCategory::Socket},
{"socket.getservbyname", AuditEventCategory::Socket},
{"socket.getservbyport", AuditEventCategory::Socket},
{"socket.sendmsg", AuditEventCategory::Socket},
{"socket.sendto", AuditEventCategory::Socket},
// processcreate
{"os.fork", AuditEventCategory::ProcessCreate},
{"os.forkpty", AuditEventCategory::ProcessCreate},
{"os.posix_spawn", AuditEventCategory::ProcessCreate},
{"os.spawn", AuditEventCategory::ProcessCreate},
{"os.system", AuditEventCategory::ProcessCreate},
{"os.startfile", AuditEventCategory::ProcessCreate},
{"os.startfile/2", AuditEventCategory::ProcessCreate},
{"pty.spawn", AuditEventCategory::ProcessCreate},
{"subprocess.Popen", AuditEventCategory::ProcessCreate},
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
};
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
static AuditEventCategory event_category(const std::string& event_name)
{
const auto it = audit_event_categories.find(event_name);
return it == audit_event_categories.end() ? AuditEventCategory::None : it->second;
}
// True for the categories whose targets are filesystem paths, as opposed to a network
// address or a process command line -- the deny-path and allowed-root checks only make sense
// against a path.
static bool is_fs_category(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead:
case AuditEventCategory::FsReadWrite:
case AuditEventCategory::FsCreate:
case AuditEventCategory::FsDelete:
return true;
default:
return false;
}
}
// ---------------------------------------------------------------------------
// Path safety
// ---------------------------------------------------------------------------
@@ -194,21 +78,24 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
// ScopedPluginAuditContext
// ---------------------------------------------------------------------------
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local bool PluginAuditManager::m_audit_denial_pending = false;
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots;
thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
const std::string& capability_name)
const std::string& capability_name,
PluginAuditManager::AuditMode mode)
: m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_capability(PluginAuditManager::instance().current_capability())
, m_previous_mode(PluginAuditManager::instance().audit_mode())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
{
PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_current_capability(capability_name);
PluginAuditManager::instance().set_audit_mode(mode);
PluginAuditManager::m_scoped_allowed_roots.clear();
}
@@ -216,6 +103,7 @@ ScopedPluginAuditContext::~ScopedPluginAuditContext()
{
PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_current_capability(m_previous_capability);
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
}
@@ -241,24 +129,23 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root)
{
if (root.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
m_global_allowed_roots.push_back({root, allow_write});
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write;
m_global_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
}
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write)
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root)
{
if (root.empty())
return;
m_scoped_allowed_roots.push_back({root, allow_write});
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string()
<< " allow_write=" << allow_write;
m_scoped_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string();
}
// ---------------------------------------------------------------------------
@@ -312,65 +199,12 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi
}
// ---------------------------------------------------------------------------
// Denied path keywords
// Audit mode
// ---------------------------------------------------------------------------
void PluginAuditManager::add_denied_path_keyword(const std::string& keyword)
{
if (keyword.empty())
return;
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; }
std::string lower = keyword;
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
std::lock_guard<std::mutex> lock(m_mutex);
m_denied_path_keywords.push_back(lower);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower;
}
std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
{
// Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin
// must never be able to reach a secret, a certificate, or a configuration file just because
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
// granted as a read-only allowed root).
return {"secret", "cert", "conf"};
}
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
{
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::path canon = fs::weakly_canonical(candidate, ec);
if (ec) {
canon = fs::absolute(candidate, ec).lexically_normal();
if (ec)
canon = candidate;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (m_denied_path_keywords.empty())
return false;
for (const auto& component : canon) {
std::string name = component.string();
if (name.empty())
continue;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
for (const auto& keyword : m_denied_path_keywords) {
if (name.find(keyword) != std::string::npos)
return true;
}
}
return false;
}
bool PluginAuditManager::is_denied_path(const boost::filesystem::path& candidate) const
{
return is_denied_filename(candidate) || is_denied_path_keyword(candidate);
}
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; }
// ---------------------------------------------------------------------------
// Policy checks
@@ -385,20 +219,20 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
if (plugin_key.empty())
return {true, ""}; // not running inside a plugin context
// Denied filenames/keywords are checked before the allowed roots. The app config and the
// cloud refresh token live directly inside data_dir(), which is a global allowed root, and
// the bundled TLS client cert lives inside resources_dir(), a read-only global allowed
// root, so a deny placed any lower would be unreachable.
// Denied filenames are checked first, above both the Loading exemption below and the
// allowed roots. The app config and the cloud refresh token live directly inside
// data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a
// deny placed any lower would be unreachable for reads.
if (is_denied_filename(path)) {
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
<< " plugin=" << plugin_key << " reason=denied filename";
return {false, "denied filename"};
}
if (is_denied_path_keyword(path)) {
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
<< " plugin=" << plugin_key << " reason=denied path keyword";
return {false, "denied path keyword"};
}
// During import/loading, only block writes. Python must be able to read
// stdlib modules and the plugin file itself during import.
if (m_audit_mode == AuditMode::Loading && !is_write)
return {true, ""};
namespace fs = boost::filesystem;
fs::path candidate = path;
@@ -411,11 +245,8 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
candidate = absolute_candidate;
}
// 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.
for (const auto& root : m_scoped_allowed_roots) {
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
if (is_inside_allowed_root(candidate, root)) {
return {true, ""};
}
}
@@ -423,13 +254,14 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& root : m_global_allowed_roots) {
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
if (is_inside_allowed_root(candidate, root)) {
return {true, ""};
}
}
}
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
<< " plugin=" << plugin_key;
return {false, "outside allowed root"};
}
@@ -437,99 +269,19 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
{
const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos ||
mode.find('+') != std::string::npos || mode.find('x') != std::string::npos;
mode.find('+') != std::string::npos;
return check_path_access(boost::filesystem::path(path_str), is_write);
}
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
if (plugin_key.empty() || paths.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
}
if (!granted)
return false;
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::report_violation(const AuditViolation& violation)
{
m_last_violation = violation;
m_has_last_violation = true;
m_audit_denial_pending = true;
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
<< " reason=" << violation.reason;
<< " path=" << violation.path.string() << " reason=" << violation.reason;
}
bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; }
void PluginAuditManager::clear_audit_denial() { m_audit_denial_pending = false; }
void PluginAuditManager::clear_last_violation()
{
m_has_last_violation = false;
@@ -545,326 +297,31 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
return true;
}
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids) const
{
if (plugin_key.empty() || call_site_ids.empty())
return false;
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_approved_call_sites.find(plugin_key);
if (it == m_approved_call_sites.end())
return false;
for (const auto& id : call_site_ids)
if (it->second.count(id))
return true;
return false;
}
void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids)
{
if (plugin_key.empty() || call_site_ids.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
auto& approved = m_approved_call_sites[plugin_key];
approved.insert(call_site_ids.begin(), call_site_ids.end());
}
// ---------------------------------------------------------------------------
// The C-level audit hook
// ---------------------------------------------------------------------------
namespace PluginAuditDetail {
PyObject* tuple_item(PyObject* args, Py_ssize_t index)
{
if (!args || !PyTuple_Check(args) || index < 0 || index >= PyTuple_GET_SIZE(args))
return nullptr;
return PyTuple_GET_ITEM(args, index);
}
std::string python_path(PyObject* object)
{
if (!object)
return {};
PyObject* path_object = PyOS_FSPath(object);
if (!path_object) {
PyErr_Clear();
return {};
}
const char* path = PyUnicode_Check(path_object) ? PyUnicode_AsUTF8(path_object) : PyBytes_AsString(path_object);
std::string result = path ? path : "";
Py_DECREF(path_object);
if (!path)
PyErr_Clear();
return result;
}
std::string python_unicode(PyObject* object)
{
if (!object || !PyUnicode_Check(object))
return {};
const char* text = PyUnicode_AsUTF8(object);
if (!text) {
PyErr_Clear();
return {};
}
return text;
}
std::string python_str(PyObject* object)
{
if (!object)
return {};
PyObject* str_object = PyObject_Str(object);
if (!str_object) {
PyErr_Clear();
return {};
}
const std::string result = python_unicode(str_object);
Py_DECREF(str_object);
return result;
}
std::vector<std::string> call_site_identities(const std::string& plugin_root)
{
std::vector<std::string> ids;
if (plugin_root.empty())
return ids;
PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference
Py_XINCREF(frame); // normalize to an owned reference for the loop below
while (frame) {
PyCodeObject* code = PyFrame_GetCode(frame); // new reference
const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename));
const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root);
std::string id;
if (!is_plugin_frame && !filename.empty()) {
const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name));
id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno);
}
Py_DECREF(code);
PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack
Py_DECREF(frame);
frame = back;
if (is_plugin_frame)
break;
if (!id.empty())
ids.push_back(std::move(id));
}
Py_XDECREF(frame); // only holds a reference here if the loop exited via break
return ids;
}
static const std::unordered_set<std::string> two_path_fs_events{
"os.rename", "os.link", "os.symlink", "shutil.copyfile",
"shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move",
"shutil.unpack_archive", "_winapi.CreateJunction",
};
static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_target_arg_indices{
{"http.client.connect", {1}},
{"urllib.Request", {0}},
{"socket.connect", {1}},
{"socket.bind", {1}},
{"socket.getaddrinfo", {0}},
{"socket.gethostbyname", {0}},
{"socket.gethostbyaddr", {0}},
{"socket.getnameinfo", {0}},
{"socket.getservbyname", {0}},
{"socket.getservbyport", {0}},
{"os.system", {0}},
{"subprocess.Popen", {1, 0}},
{"os.posix_spawn", {1, 0}},
{"os.spawn", {2, 1}},
{"os.startfile", {0}},
{"pty.spawn", {0}},
{"_winapi.CreateProcess", {1, 0}},
{"_posixsubprocess.fork_exec", {0}},
};
AuditEventCategory open_category(PyObject* args)
{
const std::string mode = python_unicode(tuple_item(args, 1));
if (!mode.empty() && mode.find_first_of("wax+") == std::string::npos)
return AuditEventCategory::FsRead;
return AuditEventCategory::FsReadWrite;
}
std::vector<std::string> audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args)
{
std::vector<std::string> targets;
switch (category) {
case AuditEventCategory::FsRead:
case AuditEventCategory::FsReadWrite:
case AuditEventCategory::FsCreate:
case AuditEventCategory::FsDelete: {
const Py_ssize_t path_count = two_path_fs_events.count(event_name) ? 2 : 1;
for (Py_ssize_t index = 0; index < path_count; ++index) {
const std::string path = python_path(tuple_item(args, index));
if (!path.empty())
targets.push_back(path);
}
return targets;
}
default:
break;
}
const auto it = audit_target_arg_indices.find(event_name);
if (it != audit_target_arg_indices.end()) {
for (const Py_ssize_t index : it->second) {
std::string value = python_str(tuple_item(args, index));
if (!value.empty()) {
targets.push_back(std::move(value));
break;
}
}
}
return targets;
}
std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions)
{
switch (category) {
case AuditEventCategory::FsRead: return &permissions.fs_read;
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process;
default: return nullptr;
}
}
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
{
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
const std::string& target)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
if (!has_permission(permission_list, target))
permission_list.push_back(target);
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
namespace {
// Records a blocked event and raises PermissionError in the calling interpreter. Returns -1
// so an event branch can `return report_denied(...)` directly.
int report_denied(PluginAuditManager& mgr,
const std::string& event_name,
const boost::filesystem::path& path,
const AuditDecision& decision)
{
AuditViolation violation;
violation.plugin_key = mgr.current_plugin();
violation.event_name = event_name;
violation.path = path;
violation.reason = decision.reason;
mgr.report_violation(violation);
PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path");
return -1;
}
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
const wxString& target_list)
{
if (target_list.IsEmpty())
return wxString::Format(
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
"This operation does not expose a target the audit hook can display."),
plugin_name, event_name);
switch (category) {
case AuditEventCategory::FsRead:
return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsReadWrite:
return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsDelete:
return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::Http:
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
case AuditEventCategory::Socket:
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);
default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
}
}
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets,
std::vector<std::string>* permission_list,
const std::vector<std::string>& call_site_ids)
{
std::vector<std::string> unresolved = targets;
if (permission_list) {
unresolved.clear();
for (const auto& target : targets)
if (!has_permission(*permission_list, target))
unresolved.push_back(target);
if (!targets.empty() && unresolved.empty())
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)
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& target : unresolved)
persist_permission(plugin_key, state, *permission_list, target);
mgr.record_approved_call_sites(plugin_key, call_site_ids);
return 0;
}
} // namespace PluginAuditDetail
} // namespace
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{
@@ -874,94 +331,93 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
std::string event_name(event ? event : "");
if (event_name.empty())
return 0;
// Verbose logging of every audit event (can be noisy)
if (mgr->verbose_events) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
}
if (mgr->current_plugin().empty()) {
BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)";
return 0;
}
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
// the open function can take in different flags that determine if it is a read or readwrite.
const AuditEventCategory event_type =
event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
if (event_type == AuditEventCategory::None)
return 0;
// --- open event ---
if (event_name == "open") {
const char* path_cstr = nullptr;
const char* mode_cstr = nullptr;
int flags = 0;
const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the ancestor-cascade check below, so a cascade
// approval recorded for an unrelated action can never launder access to one, and before
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
// folder) cannot make a denied path underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
}
}
PluginDescriptor plugin_descriptor;
PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
// Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
// was already approved, socket.connect won't trigger another permission dialog.
const std::vector<std::string> call_site_ids =
PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
// open(path, mode, flags) — path may be str, bytes, or int fd
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
}
std::string path_str(path_cstr ? path_cstr : "");
std::string mode_str(mode_cstr ? mode_cstr : "r");
AuditDecision decision = mgr->check_open(path_str, mode_str);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
return 0;
}
PluginInstallState state;
const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin();
// --- os.rename event (raised by os.rename and os.replace) ---
if (event_name == "os.rename") {
const char* src_cstr = nullptr;
const char* dst_cstr = nullptr;
PyObject* src_dir_fd = nullptr;
PyObject* dst_dir_fd = nullptr;
// os.rename(src, dst, src_dir_fd, dst_dir_fd) — paths may be str, bytes, or int fd.
// The dir_fd arguments are unused, but must be accepted for the tuple to parse.
if (!PyArg_ParseTuple(args, "ss|OO", &src_cstr, &dst_cstr, &src_dir_fd, &dst_dir_fd)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
}
// A rename writes at both ends, so either end being denied blocks the call.
for (const char* path_cstr : {src_cstr, dst_cstr}) {
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
}
return 0;
}
const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
// --- os.remove event (raised by os.remove and os.unlink) ---
if (event_name == "os.remove") {
const char* path_cstr = nullptr;
PyObject* dir_fd = nullptr;
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
// os.remove(path, dir_fd) — path may be str, bytes, or int fd
if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
}
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
event_type, targets, permission_list, call_site_ids);
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
return 0;
}
// Unknown event — allow by default
return 0;
}
void PluginAuditManager::install_hook()
{
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
// covers the plugin system's own workflow needs -- each plugin's storage folder
// (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
// without a separate, narrower grant for either (G-code plugins additionally get the temp
// G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline).
add_global_allowed_root(data_dir());
if (PySys_AddAuditHook(audit_hook, this) < 0) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
return;
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
// resources_dir() holds the app's bundled, shared assets (installed system profiles, the
// bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled
// profiles -- but must never write into the shared, potentially multi-user app install, so
// it is granted read-only. The bundled cert itself stays unreachable regardless, via the
// "cert" denied-path keyword seeded below.
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
// data_dir() is the only globally-allowed root during enforced plugin execution.
// The executable directory and resources directory are intentionally NOT allowed
// here: plugins must not write outside data_dir() (G-code plugins additionally get
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
add_global_allowed_root(data_dir());
// The user's app config and cloud credentials live directly inside data_dir(), so the
// root just granted would otherwise expose them to any plugin. Deny them by name.
@@ -974,19 +430,6 @@ void PluginAuditManager::install_hook()
// (see its comment for why all four config names are denied); the tests seed from it too.
for (const auto& name : default_denied_filenames())
add_denied_filename(name);
// Categorical denies on top of the exact-name list above: no path a plugin can reach may
// contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which
// allowed root it happens to sit inside. default_denied_path_keywords() is the single
// source of this list; the tests seed from it too.
for (const auto& keyword : default_denied_path_keywords())
add_denied_path_keyword(keyword);
if (PySys_AddAuditHook(audit_hook, this) < 0) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
return;
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
}
} // namespace Slic3r

View File

@@ -2,11 +2,8 @@
#define slic3r_PluginAuditManager_hpp_
#include <Python.h>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <boost/filesystem.hpp>
@@ -21,31 +18,10 @@ struct AuditDecision {
struct AuditViolation {
std::string plugin_key;
std::string event_name;
boost::filesystem::path path;
std::string reason;
};
// A filesystem root a plugin may access while an audit context is active. allow_write is
// false for a root that only grants reads (e.g. the bundled, shared resources folder) --
// a write-shaped event never matches such a root, even though a read-shaped one does.
struct AllowedRoot {
boost::filesystem::path path;
bool allow_write = true;
};
// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of
// operation they represent. None means the event isn't one audit_hook() acts on at all.
enum class AuditEventCategory {
None,
FsRead,
FsReadWrite,
FsCreate,
FsDelete,
Http,
Socket,
ProcessCreate,
Threading,
};
// Returns true if candidate resolves to a path inside allowed_root.
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
bool is_inside_allowed_root(const boost::filesystem::path& candidate,
@@ -73,15 +49,12 @@ public:
void clear_current_capability();
// --- allowed-roots registry ---
// allow_write = false registers a read-only root: a read-shaped event inside it is allowed,
// but a write/create/delete-shaped event is not, so it falls through to the normal
// prompt-or-deny path instead.
void add_global_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
void add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
void add_global_allowed_root(const boost::filesystem::path& root);
void add_scoped_allowed_root(const boost::filesystem::path& root);
// --- denied-filenames registry ---
// Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed
// root. A candidate is denied when its filename starts with a
// Filenames a plugin may never touch, in any directory, regardless of audit mode or
// enclosing allowed root. A candidate is denied when its filename starts with a
// registered name, so .bak/.tmp companions are covered by the same entry.
//
// The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals
@@ -100,60 +73,33 @@ public:
// 8.3 short name is out of scope (see the design doc). This blocks direct access only.
bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- denied-path-keyword registry ---
// Keywords that categorically deny a path if ANY of its components (directory or file
// name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
// subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
// otherwise reach, including inside an allowed root. This is intentionally broader and
// fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
// out whole classes of sensitive paths (secrets, certificates, config) rather than name
// specific known files, at the cost of over-blocking an unrelated name that happens to
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
void add_denied_path_keyword(const std::string& keyword);
// --- enforcement mode ---
enum class AuditMode {
// Import/loading phase: allow reads anywhere, only block writes
// outside allowed roots. Python needs to read stdlib modules
// during import and those are not inside plugin directories.
Loading,
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
// same set without a live interpreter.
static std::vector<std::string> default_denied_path_keywords();
// Execution phase: block both reads and writes outside allowed
// roots, plus subprocess/socket/ctypes.
Enforcing,
};
// True when any component of candidate's (canonicalized) path contains a registered
// keyword, case-insensitively.
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
// call sites that only need to know whether a path is categorically off-limits, not which
// specific rule fired.
bool is_denied_path(const boost::filesystem::path& candidate) const;
void set_audit_mode(AuditMode mode);
AuditMode audit_mode() const;
// --- policy checks ---
// Shared core for every audited filesystem event. The deny checks are consulted above the
// allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g.
// data_dir(), which is a global allowed root).
// Shared core for every audited filesystem event. The deny list is consulted above the
// Loading-mode read exemption and above the allowed roots, so a denied filename is
// blocked even though every scope currently runs in Loading and the files in question
// sit inside data_dir(), which is itself a global allowed root.
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode);
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
// load without adding a permission.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const;
void clear_audit_denial();
void clear_last_violation();
bool last_violation(AuditViolation& violation) const;
// --- call-site cascade cache ---
// A single plugin action often fires several nested CPython audit events as it passes
// through stdlib layers (urllib.request calling http.client calling socket, for example).
// Once the user approves one event, every stdlib frame still on the stack for that call
// is recorded here by (filename, function, first line) identity. A later event whose own
// ancestor chain still contains one of those frames is the same logical action seen from
// a deeper layer, so it is auto-approved instead of prompting again.
bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const;
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
bool verbose_events = true;
private:
@@ -163,19 +109,17 @@ private:
static int audit_hook(const char* event, PyObject* args, void* user_data);
static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name;
static thread_local AuditMode m_audit_mode;
static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
// mutable: is_denied_filename() is a const query that must lock.
mutable std::mutex m_mutex;
std::vector<AllowedRoot> m_global_allowed_roots;
std::vector<boost::filesystem::path> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames;
std::vector<std::string> m_denied_path_keywords;
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
};
// RAII guard that sets the current plugin key and capability name, restoring the previous
@@ -186,7 +130,8 @@ class ScopedPluginAuditContext
public:
explicit ScopedPluginAuditContext(
const std::string& plugin_key,
const std::string& capability_name = {});
const std::string& capability_name = {},
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
~ScopedPluginAuditContext();
@@ -196,7 +141,8 @@ public:
private:
std::string m_previous_id;
std::string m_previous_capability;
std::vector<AllowedRoot> m_previous_scoped_roots;
PluginAuditManager::AuditMode m_previous_mode;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
};
} // namespace Slic3r

View File

@@ -784,23 +784,6 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
parsed.plugin_name = state["plugin_name"].get<std::string>();
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
if (state.contains("permissions") && state["permissions"].is_object()) {
const auto& permissions = state["permissions"];
auto read_string_list = [&permissions](const char* key, std::vector<std::string>& out) {
if (!permissions.contains(key) || !permissions[key].is_array())
return;
for (const auto& entry : permissions[key])
if (entry.is_string())
out.push_back(entry.get<std::string>());
};
read_string_list("fs_read", parsed.permissions.fs_read);
read_string_list("fs_readwrite", parsed.permissions.fs_readwrite);
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);
}
if (state.contains("enabled") && state["enabled"].is_boolean())
parsed.enabled = state["enabled"].get<bool>();
@@ -836,14 +819,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
if (!state.cloud_uuid.empty())
json["cloud_uuid"] = state.cloud_uuid;
json["permissions"] = {
{"fs_read", state.permissions.fs_read},
{"fs_readwrite", state.permissions.fs_readwrite},
{"network_http", state.permissions.network_http},
{"network_socket", state.permissions.network_socket},
{"process", state.permissions.process},
};
nlohmann::json capabilities = nlohmann::json::array();
for (const auto& [name, enabled] : state.capabilities)
capabilities.push_back(nlohmann::json{{name, enabled}});
@@ -861,9 +836,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
const std::vector<std::pair<std::string, bool>>& capabilities)
{
PluginInstallState state;
// Loading a plugin updates its lifecycle/capability state, but must retain permissions granted
// during register_capabilities() or by a previous runtime audit prompt.
read_install_state(plugin_dir, state);
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud
// at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
@@ -880,16 +852,10 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
{
// Install-time writer: the package is not loaded, so its capabilities are not known yet and the
// sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets
// permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags
// while preserving permissions once the package is loaded, via the overload above.
PluginInstallState state;
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
state.plugin_name = entry.name;
state.cloud_uuid = entry.cloud_uuid();
state.enabled = true;
return write_install_state(plugin_dir, state);
// sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
// per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
// overload.
return write_install_state(plugin_dir, entry, true, {});
}
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)

View File

@@ -82,23 +82,11 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
return py::str(o).cast<std::string>(); // fallback: str()
}
struct PluginPermissions
{
std::vector<std::string> fs_read;
std::vector<std::string> fs_readwrite;
std::vector<std::string> network_http;
std::vector<std::string> network_socket;
std::vector<std::string> process;
};
struct PluginInstallState {
std::string installed_from; // "local" | "cloud"
std::string installed_version;
std::string plugin_name;
std::string cloud_uuid; // empty for local
PluginPermissions permissions;
bool enabled = true;
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
};

View File

@@ -280,7 +280,7 @@ bool load(const PluginDescriptor& descriptor,
// (while the active plugin key is set), then instantiates each registered capability and caches
// its get_name(). Returns one entry per capability.
std::string bridge_error;
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error);
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error);
if (!bridge_error.empty()) {
capabilities_found.clear();
error = "Plugin registration failed: " + bridge_error;

View File

@@ -21,7 +21,6 @@
#include <chrono>
#include <mutex>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
@@ -623,20 +622,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
return nullptr;
}
bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state)
{
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
return false;
}
if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) {
return false;
}
return true;
}
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
@@ -925,8 +910,6 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) {
if (error == LOAD_CANCELLED)
return; // cancelled: nothing materialized survives, and no error is recorded
if (error.rfind("Plugin registration failed:", 0) == 0)
mark_plugin_install_state_disabled(plugin_key);
fail(std::move(error));
return;
}
@@ -1152,51 +1135,6 @@ void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_
write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities);
}
void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
}
state.enabled = false;
if (!write_install_state(root, state))
return;
std::lock_guard<std::mutex> lock(m_mutex);
if (Plugin* plugin = find_plugin_locked(plugin_key))
plugin->descriptor.enabled = false;
}
void PluginManager::revoke_plugin_permissions(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key;
return;
}
state.permissions = {};
if (!write_install_state(root, state))
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key;
}
// ── Callbacks ───────────────────────────────────────────────────────────────────────────────
void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn)
@@ -1416,11 +1354,6 @@ bool PluginManager::install_plugin(const boost::filesystem::path& filepath, Plug
return false;
}
// Every successful install may have replaced executable plugin code. Revoke any permissions
// associated with the previous package so the newly installed version must request them again.
if (!plugin_descriptor.plugin_key.empty())
revoke_plugin_permissions(plugin_descriptor.plugin_key);
if (!plugin_descriptor.plugin_key.empty())
clear_plugin_error(plugin_descriptor.plugin_key);
@@ -1808,7 +1741,6 @@ bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::stri
}
clear_plugin_error(plugin_key);
return true;
}

View File

@@ -159,8 +159,6 @@ public:
PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const;
bool get_install_state(const std::string& plugin_key, PluginInstallState& install_state);
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
bool unload_plugin(const std::string& plugin_key);
void unload_all_plugins();
@@ -257,9 +255,6 @@ private:
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
void write_loaded_plugin_install_state(const std::string& plugin_key);
void mark_plugin_install_state_disabled(const std::string& plugin_key);
// Revoke permissions after a package replacement so the new package must request them again.
void revoke_plugin_permissions(const std::string& plugin_key);
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);

View File

@@ -26,30 +26,25 @@
try { \
override_call; \
} catch (pybind11::error_already_set & err) { \
const bool _orca_audit_denial = ::Slic3r::PluginAuditManager::instance().audit_denial_pending(); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_current_plugin(); \
::Slic3r::log_python_exception_keep(err); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_audit_denial(); \
throw; \
}
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
// the calling capability's cached name so host APIs invoked from Python can tell which capability
// they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`.
#define ORCA_PY_AUDIT_SCOPE() \
#define ORCA_PY_AUDIT_SCOPE(mode) \
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
_orca_audit_scope.emplace(_orca_audit_key, this->name())
_orca_audit_scope.emplace(_orca_audit_key, this->name(), mode)
#define ORCA_PY_OVERRIDE_AUDITED(audit_setup, override_macro, ret, base, name, ...) \
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
do { \
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
::Slic3r::PythonGILState _orca_python_gil; \
if (!_orca_python_gil) \
throw std::runtime_error("Python interpreter is shutting down"); \
ORCA_PY_AUDIT_SCOPE(); \
ORCA_PY_AUDIT_SCOPE(mode); \
if (_orca_audit_scope) \
audit_setup(); \
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
@@ -63,7 +58,7 @@ public:
std::string get_name() const override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
}
// Config UI hooks. Available on every capability type, so they live here rather than in
@@ -71,6 +66,7 @@ public:
bool has_config_ui() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
bool,
@@ -81,6 +77,7 @@ public:
std::string get_config_ui() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
std::string,
@@ -97,7 +94,7 @@ public:
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override
{
ORCA_PY_AUDIT_SCOPE();
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
try {
pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
@@ -120,17 +117,17 @@ public:
void on_load() override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load);
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
}
void on_unload() override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
}
void on_cancelled() override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
}
};
@@ -142,7 +139,7 @@ public:
PluginCapabilityType get_type() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
get_type);
}
};

View File

@@ -87,12 +87,6 @@ void log_python_exception_keep(pybind11::error_already_set& err)
if (!gil)
return;
// Traceback output is host-owned work. In particular, the stderr tee opens the Python log
// file on every write. Keep that open outside the plugin audit context, otherwise an ordinary
// exception raised by a plugin can recursively trigger the filesystem permission dialog while
// its original exception is being reported.
ScopedPluginAuditContext audit_suppression("");
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
// catchers can still read err.what() for the user-facing dialog. We must NOT use
@@ -628,6 +622,10 @@ bool PythonInterpreter::initialize()
else
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
// Install the CPython audit hook for plugin policy enforcement.
// This is defense-in-depth: today it only inspects the `open` audit event
// and blocks writes outside the allowed roots; subprocess/socket/ctypes and
// other events are not yet handled. It is NOT a full security sandbox.
PluginAuditManager::instance().install_hook();
// Persist Python stderr (plugin tracebacks, including uncaught

View File

@@ -1,6 +1,5 @@
#include "PythonPluginBridge.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <exception>
#include <memory>
@@ -41,7 +40,6 @@ thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package;
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
struct PluginInstanceHandle
{
// The C++ plugin interface points into a Python object. Keep both alive through one
@@ -79,8 +77,6 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
(void) package->second.release();
g_pending_package.erase(package);
}
g_pending_fs_read_permissions.erase(plugin_key);
}
} // namespace
@@ -105,38 +101,34 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
// for this same entry path.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
}
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin.
g_active_plugin_key = plugin_key;
}
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key,
const std::string& plugin_key,
std::string& error)
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
{
PythonGILState gil;
if (!gil) {
error = "Python interpreter is shutting down";
return {};
}
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_key;
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
// Phase 1: run the package class's register_capabilities() while the active key is
// still set. That method is expected to call orca.register_capability() once per
// capability class, and register_capability() needs g_active_plugin_key to know which
// pending bucket to append to.
{
auto clear_active_key = [&capture_key]() {
if (g_active_plugin_key == capture_key)
auto clear_active_key = [&plugin_key]() {
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
};
auto discard_pending_for_key = [&capture_key]() {
auto discard_pending_for_key = [&plugin_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(capture_key);
g_pending_package.erase(capture_key);
g_pending_fs_read_permissions.erase(capture_key);
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
};
try {
@@ -146,7 +138,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object package_cls;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_package.find(capture_key);
auto it = g_pending_package.find(plugin_key);
if (it != g_pending_package.end()) {
package_cls = it->second;
g_pending_package.erase(it);
@@ -165,33 +157,6 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// are kept.
py::object package = package_cls();
package.attr("register_capabilities")();
// Permission declarations are collected while register_capabilities() runs so the
// plugin can describe its needs without touching wx from the Python load worker. Ask
// only after registration returns, before capability instances are materialized.
std::vector<std::string> fs_read_permissions;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_fs_read_permissions.find(capture_key);
if (it != g_pending_fs_read_permissions.end()) {
fs_read_permissions = std::move(it->second);
g_pending_fs_read_permissions.erase(it);
}
}
if (!fs_read_permissions.empty()) {
bool granted = false;
{
py::gil_scoped_release release;
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
}
if (!granted) {
error = "Plugin filesystem read permission request denied";
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
return {};
}
}
} catch (py::error_already_set& err) {
log_python_exception_keep(err);
error = err.what();
@@ -215,7 +180,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
std::vector<py::object> classes;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_capabilities.find(capture_key);
auto it = g_pending_capabilities.find(plugin_key);
if (it != g_pending_capabilities.end()) {
classes = std::move(it->second);
g_pending_capabilities.erase(it);
@@ -224,10 +189,10 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// Registration is complete. Later register_capability() calls should fail instead of
// accidentally attaching themselves to this plugin.
if (g_active_plugin_key == capture_key)
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key;
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size());
@@ -239,7 +204,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object instance = cls();
if (!py::isinstance<PluginCapabilityInterface>(instance)) {
error = "Registered capability must inherit from a PluginCapability base";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
<< " error=" << error;
return {};
}
@@ -247,7 +212,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
if (!capability_iface) {
error = "Failed to cast Python capability to PluginCapabilityInterface";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
<< " error=" << error;
return {};
}
@@ -261,7 +226,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// here is a hard error that rejects the whole plugin capture.
if (name.find(';') != std::string::npos) {
error = "Capability name must not contain ';': " + name;
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
<< " error=" << error;
return {};
}
@@ -281,12 +246,12 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// log the traceback here. GIL is held for the duration of finalize_plugin_capture.
log_python_exception_keep(err);
error = err.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_key
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
<< " error=" << error;
return {};
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
<< " error=" << error;
return {};
}
@@ -314,7 +279,6 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
// may already have registered under this key.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
}
if (g_active_plugin_key == plugin_key)
@@ -341,7 +305,6 @@ void PythonPluginBridge::clear_pending_captures()
(void) pkg.release();
}
g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear();
return;
}
@@ -350,7 +313,6 @@ void PythonPluginBridge::clear_pending_captures()
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear();
g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear();
}
@@ -488,27 +450,6 @@ void bind_python_api(pybind11::module_& m)
},
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
m.def(
"request_permissions",
[](const std::vector<std::string>& fs_read) {
if (g_active_plugin_key.empty())
throw py::value_error("request_permissions() called outside plugin discovery context");
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
for (const std::string& path : fs_read) {
if (path.empty())
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
if (std::find(requested.begin(), requested.end(), path) == requested.end())
requested.push_back(path);
}
},
py::arg("fs_read") = std::vector<std::string>{},
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
The host presents the request to the user after register_capabilities() returns. Denying the
request aborts the plugin load.)pbdoc");
m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty())
throw py::value_error("@orca.plugin used outside plugin discovery context");

View File

@@ -31,7 +31,7 @@ public:
// Returns one CapturedCapability per capability, or an empty vector on failure
// (error message populated).
std::vector<CapturedCapability> finalize_plugin_capture(
const std::string& capture_key, const std::string& plugin_key, std::string& error);
const std::string& plugin_key, std::string& error);
// Clear any pending registrations for the key. Safe to call when import fails.
void cancel_plugin_capture(const std::string& plugin_key);

View File

@@ -16,6 +16,7 @@ public:
std::string get_icon() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
std::string,
@@ -26,6 +27,7 @@ public:
std::string get_ui() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
@@ -40,7 +42,7 @@ public:
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE();
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
if (!override)

View File

@@ -16,206 +16,206 @@ public:
AgentInfo get_agent_info() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info);
}
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl);
}
int disconnect_printer() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
}
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag);
}
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag);
}
bool start_discovery(bool start, bool sending) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending);
}
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect);
}
std::string get_user_selected_machine() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine);
}
int set_user_selected_machine(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id);
}
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn);
}
FilamentSyncMode get_filament_sync_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode);
}
bool fetch_filament_info(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
}
int check_cert() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
}
void install_device_cert(std::string dev_id, bool lan_only) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only);
}
int ping_bind(std::string ping_code) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
}
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn);
}
int unbind(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
}
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn);
}
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
}
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn);
}
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback);
}
int set_server_callback(OnServerErrFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
}
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
}
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn);
}
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn);
}
int set_on_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
}
int set_on_user_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
}
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
}
int set_on_local_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
}
int set_queue_on_main_fn(QueueOnMainFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
}
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
@@ -223,7 +223,7 @@ public:
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
int request_bind_ticket(std::string* ticket) override
{
ORCA_PY_AUDIT_SCOPE();
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)

View File

@@ -13,6 +13,7 @@ public:
ExecutionResult execute() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE_PURE,
ExecutionResult,

View File

@@ -10,6 +10,7 @@ public:
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[&]{
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
// outside data_dir() (a temp/output folder), so writing to it would otherwise be

View File

@@ -34,24 +34,4 @@ struct ScopedDataDir
ScopedDataDir& operator=(const ScopedDataDir&) = delete;
};
// Point resources_dir() at a throwaway directory for the lifetime of a test and restore the
// previous value afterwards, mirroring ScopedDataDir.
struct ScopedResourcesDir
{
ScopedTemporaryDir tmp;
boost::filesystem::path dir;
std::string previous;
explicit ScopedResourcesDir(const std::string& tag)
: tmp("orca-" + tag), dir(tmp.path()), previous(resources_dir())
{
set_resources_dir(dir.string());
}
~ScopedResourcesDir() { set_resources_dir(previous); }
ScopedResourcesDir(const ScopedResourcesDir&) = delete;
ScopedResourcesDir& operator=(const ScopedResourcesDir&) = delete;
};
} // namespace Slic3r

View File

@@ -27,16 +27,6 @@ void seed_denied_names()
mgr.add_denied_filename(name);
}
// Seed the keyword registry with the same list install_hook() uses. Same rationale as
// seed_denied_names(): a process-singleton registry, seeded from the single shared source so
// production and tests cannot drift apart.
void seed_denied_keywords()
{
PluginAuditManager& mgr = PluginAuditManager::instance();
for (const auto& keyword : PluginAuditManager::default_denied_path_keywords())
mgr.add_denied_path_keyword(keyword);
}
} // namespace
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
@@ -91,7 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
}
}
TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-deny");
seed_denied_names();
@@ -101,8 +91,8 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
// config and the token would otherwise be reachable simply by living inside it.
mgr.add_global_allowed_root(data_dir());
// Enter a plugin context. The deny must hold even inside a globally allowed root.
ScopedPluginAuditContext ctx("test_plugin", "");
// Enter a plugin context. The deny must hold in Loading mode, which every scope runs in.
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
@@ -113,13 +103,6 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.allowed);
}
SECTION("a file outside the allowed root is blocked for reads as well as writes")
{
AuditDecision decision = mgr.check_open((fs::path(data_dir()).parent_path() / "outside.txt").string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("writing the app config is blocked despite data_dir() being allowed")
{
AuditDecision decision = mgr.check_open(conf.string(), "w");
@@ -127,14 +110,16 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.reason == "denied filename");
}
SECTION("reading the app config is blocked despite the allowed root")
SECTION("reading the app config is blocked even though Loading exempts reads")
{
// Without the deny, a read in Loading mode short-circuits to allow. The deny sits above
// that exemption, so this must still be blocked.
AuditDecision decision = mgr.check_open(conf.string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied filename");
}
SECTION("reading the cloud refresh token is blocked")
SECTION("reading the cloud refresh token is blocked in Loading mode")
{
AuditDecision decision = mgr.check_open(token.string(), "r");
CHECK_FALSE(decision.allowed);
@@ -165,7 +150,7 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]")
const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin";
fs::create_directories(plugin_dir);
ScopedPluginAuditContext ctx("test_plugin", "");
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
mgr.add_scoped_allowed_root(plugin_dir);
SECTION("the plugin's own non-denied file opens for read and write")
@@ -200,139 +185,3 @@ TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]")
CHECK(mgr.check_open(conf.string(), "w").allowed);
CHECK(mgr.check_open(conf.string(), "r").allowed);
}
TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", "[audit]")
{
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a 'secrets' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secrets/api_key.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secret/token.txt")));
}
SECTION("a 'certificate(s)' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/cert/slicer_base64.cer")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
}
SECTION("a 'conf'/'config' directory or file component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
}
SECTION("matching is case-insensitive")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
}
SECTION("matching is not limited to the base name -- any ancestor component counts")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/data/secrets/nested/deep/file.txt")));
}
SECTION("an unrelated path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
}
SECTION("an empty path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path()));
}
}
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
{
seed_denied_names();
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a filename-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path(SLIC3R_APP_KEY ".conf")));
}
SECTION("a keyword-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path("/plugin/secrets/token.txt")));
}
SECTION("a path matching neither registry is not denied")
{
CHECK_FALSE(mgr.is_denied_path(fs::path("/plugin/output/model.gcode")));
}
}
TEST_CASE("Plugin audit a read-only allowed root blocks writes but not reads", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-readonly");
ScopedResourcesDir resources_dir_guard("plugin-audit-readonly-resources");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false);
ScopedPluginAuditContext ctx("test_plugin", "");
const fs::path readonly_file = fs::path(resources_dir()) / "profiles" / "vendor.json";
SECTION("a read inside the read-only root is allowed")
{
CHECK(mgr.check_open(readonly_file.string(), "r").allowed);
}
SECTION("a write inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_open(readonly_file.string(), "w");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("a create inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_path_access(readonly_file, /*is_write=*/true);
CHECK_FALSE(decision.allowed);
}
SECTION("the bundled cert underneath the read-only root is denied even for reads")
{
const fs::path cert = fs::path(resources_dir()) / "cert" / "slicer_base64.cer";
AuditDecision decision = mgr.check_open(cert.string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied path keyword");
}
}
TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-scoped-readonly");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
const fs::path readonly_dir = fs::path(data_dir()) / "readonly_scope";
fs::create_directories(readonly_dir);
ScopedPluginAuditContext ctx("test_plugin", "");
mgr.add_scoped_allowed_root(readonly_dir, /*allow_write=*/false);
SECTION("a read inside the scoped read-only root is allowed")
{
CHECK(mgr.check_open((readonly_dir / "vendor.json").string(), "r").allowed);
}
SECTION("a write inside the scoped read-only root is blocked")
{
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
}
}

View File

@@ -112,22 +112,6 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
REQUIRE(read_install_state(plugin_dir, state));
CHECK(state.installed_version == "1.2.0");
state.permissions.fs_read = {"/path/to/read"};
state.permissions.fs_readwrite = {"/path/to/readwrite"};
state.permissions.network_http = {"https://api.example.com"};
state.permissions.network_socket = {"192.168.45.6:443"};
state.permissions.process = {"/usr/bin/curl"};
REQUIRE(write_install_state(plugin_dir, state));
// Permission data is persisted in the same sidecar as the installation metadata.
PluginInstallState persisted;
REQUIRE(read_install_state(plugin_dir, persisted));
CHECK(persisted.permissions.fs_read == state.permissions.fs_read);
CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite);
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);
// 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
// the cloud's latest version against the installed version instead of the stale header, so an
@@ -136,4 +120,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
read_install_state(plugin_dir, scanned);
CHECK(scanned.installed_version == "1.2.0");
}
}