mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 22:18:30 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2457173764 | ||
|
|
c081aa97f7 | ||
|
|
37dcc65752 | ||
|
|
cdb7eda54f | ||
|
|
e7d6cc32a7 | ||
|
|
069bd402d9 |
No files matched your search
+25
-3
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
|||||||
u )
|
u )
|
||||||
export BUILD_TARGET="universal"
|
export BUILD_TARGET="universal"
|
||||||
;;
|
;;
|
||||||
h ) echo "Usage: ./build_release_macos.sh [-d]"
|
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
|
||||||
echo " -d: Build deps only"
|
echo " -d: Build deps only"
|
||||||
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
||||||
echo " -s: Build slicer only"
|
echo " -s: Build slicer only"
|
||||||
@@ -60,13 +60,33 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
|||||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||||
|
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
|
||||||
|
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
|
||||||
|
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
: )
|
||||||
|
echo "Option -${OPTARG} requires an argument, aborting."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
* )
|
* )
|
||||||
|
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
|
||||||
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
|
||||||
|
if [[ ${OPTIND} -le $# ]] ; then
|
||||||
|
separator_index=$((OPTIND-1))
|
||||||
|
if [[ "${!separator_index}" != "--" ]] ; then
|
||||||
|
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
EXTRA_CMAKE_ARGS=("$@")
|
||||||
|
|
||||||
# Set defaults
|
# Set defaults
|
||||||
|
|
||||||
if [ -z "$ARCH" ]; then
|
if [ -z "$ARCH" ]; then
|
||||||
@@ -211,7 +231,8 @@ function build_deps() {
|
|||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||||
${CMAKE_POLICY_COMPAT}
|
${CMAKE_POLICY_COMPAT} \
|
||||||
|
"${EXTRA_CMAKE_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||||
)
|
)
|
||||||
@@ -314,7 +335,8 @@ function build_slicer() {
|
|||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||||
${CMAKE_POLICY_COMPAT}
|
${CMAKE_POLICY_COMPAT} \
|
||||||
|
"${EXTRA_CMAKE_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+6
-2
@@ -59,9 +59,13 @@ else ()
|
|||||||
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
|
||||||
|
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
|
||||||
|
# silicon whenever CGAL falls back to exact arithmetic.
|
||||||
ExternalProject_Add(dep_GMP
|
ExternalProject_Add(dep_GMP
|
||||||
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||||
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
|
||||||
|
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
||||||
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
||||||
BUILD_IN_SOURCE ON
|
BUILD_IN_SOURCE ON
|
||||||
|
|||||||
@@ -226,10 +226,10 @@ modules:
|
|||||||
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
||||||
dest: external-packages/OpenVDB
|
dest: external-packages/OpenVDB
|
||||||
|
|
||||||
# GMP 6.2.1
|
# GMP 6.3.0
|
||||||
- type: file
|
- type: file
|
||||||
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||||
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||||
dest: external-packages/GMP
|
dest: external-packages/GMP
|
||||||
|
|
||||||
# MPFR 4.2.2
|
# MPFR 4.2.2
|
||||||
|
|||||||
@@ -5632,14 +5632,10 @@ void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_mov
|
|||||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||||
for (const std::pair<int, int>& i : done) {
|
for (const std::pair<int, int>& i : done) {
|
||||||
ModelObject* mo = m_model->objects[i.first];
|
ModelObject* mo = m_model->objects[i.first];
|
||||||
|
|
||||||
if (!mo->auto_drop) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double shift_z = mo->get_instance_min_z(i.second);
|
const double shift_z = mo->get_instance_min_z(i.second);
|
||||||
//BBS: don't call translate if the z is zero
|
//BBS: don't call translate if the z is zero
|
||||||
if ((current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
if (mo->auto_drop && (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||||
m_selection.translate(i.first, i.second, shift);
|
m_selection.translate(i.first, i.second, shift);
|
||||||
mo->translate_instance(i.second, shift);
|
mo->translate_instance(i.second, shift);
|
||||||
@@ -5763,14 +5759,10 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
|||||||
for (const std::pair<int, int> &i : done) {
|
for (const std::pair<int, int> &i : done) {
|
||||||
ModelObject *mo = m_model->objects[i.first];
|
ModelObject *mo = m_model->objects[i.first];
|
||||||
|
|
||||||
if (!mo->auto_drop) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BBS: don't call translate if the z is zero
|
// BBS: don't call translate if the z is zero
|
||||||
const double shift_z = mo->get_instance_min_z(i.second);
|
const double shift_z = mo->get_instance_min_z(i.second);
|
||||||
// leave sinking instances as sinking
|
// leave sinking instances as sinking
|
||||||
if ((min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
if (mo->auto_drop && (min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||||
m_selection.translate(i.first, i.second, shift);
|
m_selection.translate(i.first, i.second, shift);
|
||||||
mo->translate_instance(i.second, shift);
|
mo->translate_instance(i.second, shift);
|
||||||
@@ -5865,14 +5857,10 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
|||||||
for (const std::pair<int, int>& i : done) {
|
for (const std::pair<int, int>& i : done) {
|
||||||
ModelObject* mo = m_model->objects[i.first];
|
ModelObject* mo = m_model->objects[i.first];
|
||||||
|
|
||||||
if (!mo->auto_drop) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//BBS: don't call translate if the z is zero
|
//BBS: don't call translate if the z is zero
|
||||||
double shift_z = mo->get_instance_min_z(i.second);
|
double shift_z = mo->get_instance_min_z(i.second);
|
||||||
// leave sinking instances as sinking
|
// leave sinking instances as sinking
|
||||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||||
Vec3d shift(0.0, 0.0, -shift_z);
|
Vec3d shift(0.0, 0.0, -shift_z);
|
||||||
m_selection.translate(i.first, i.second, shift);
|
m_selection.translate(i.first, i.second, shift);
|
||||||
mo->translate_instance(i.second, shift);
|
mo->translate_instance(i.second, shift);
|
||||||
@@ -5985,14 +5973,10 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
|
|||||||
for (const std::pair<int, int>& i : done) {
|
for (const std::pair<int, int>& i : done) {
|
||||||
ModelObject* mo = m_model->objects[i.first];
|
ModelObject* mo = m_model->objects[i.first];
|
||||||
|
|
||||||
if (!mo->auto_drop) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//BBS: don't call translate if the z is zero
|
//BBS: don't call translate if the z is zero
|
||||||
double shift_z = mo->get_instance_min_z(i.second);
|
double shift_z = mo->get_instance_min_z(i.second);
|
||||||
// leave sinking instances as sinking
|
// leave sinking instances as sinking
|
||||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD)&&(shift_z != 0.0f)) {
|
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||||
Vec3d shift(0.0, 0.0, -shift_z);
|
Vec3d shift(0.0, 0.0, -shift_z);
|
||||||
m_selection.translate(i.first, i.second, shift);
|
m_selection.translate(i.first, i.second, shift);
|
||||||
mo->translate_instance(i.second, shift);
|
mo->translate_instance(i.second, shift);
|
||||||
|
|||||||
@@ -1618,7 +1618,9 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
|||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
if (obj_idx != -1) {
|
if (obj_idx != -1) {
|
||||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||||
|
ModelObject* mo = (*m_objects)[obj_idx];
|
||||||
|
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||||
cnv->reload_scene(true, true);
|
cnv->reload_scene(true, true);
|
||||||
update_info_items(obj_idx);
|
update_info_items(obj_idx);
|
||||||
notify_instance_updated(obj_idx);
|
notify_instance_updated(obj_idx);
|
||||||
@@ -6734,7 +6736,9 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
|||||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||||
int obj_idx, vol_idx;
|
int obj_idx, vol_idx;
|
||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||||
|
ModelObject* mo = (*m_objects)[obj_idx];
|
||||||
|
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||||
cnv->reload_scene(true, true);
|
cnv->reload_scene(true, true);
|
||||||
update_info_items(obj_idx);
|
update_info_items(obj_idx);
|
||||||
notify_instance_updated(obj_idx);
|
notify_instance_updated(obj_idx);
|
||||||
|
|||||||
@@ -579,29 +579,12 @@ void Selection::drop()
|
|||||||
|
|
||||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||||
|
|
||||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
// do_move() writes every volume back to the model and refreshes the view.
|
||||||
|
setup_cache();
|
||||||
for (unsigned int i : m_list) {
|
TransformationType transformation_type;
|
||||||
GLVolume& volume = *(*m_volumes)[i];
|
transformation_type.set_relative();
|
||||||
// Skip the wipe tower: its synthetic id (>= 1000) is not an index into m_model->objects.
|
translate(Vec3d(0, 0, -min_z), transformation_type);
|
||||||
if (volume.object_idx() >= 1000)
|
wxGetApp().plater()->get_view3D_canvas3D()->do_move("");
|
||||||
continue;
|
|
||||||
ModelObject* model_object = m_model->objects[volume.object_idx()];
|
|
||||||
|
|
||||||
if (model_object != nullptr) {
|
|
||||||
if (m_mode == Volume) {
|
|
||||||
ModelVolume* cur_mv = model_object->volumes[volume.volume_idx()];
|
|
||||||
cur_mv->set_transformation(volume.get_volume_transformation());
|
|
||||||
} else if (m_mode == Instance) {
|
|
||||||
model_object->instances[volume.instance_idx()]->set_transformation(volume.get_instance_transformation());
|
|
||||||
}
|
|
||||||
|
|
||||||
model_object->invalidate_bounding_box();
|
|
||||||
this->notify_instance_update(volume.object_idx(), volume.instance_idx());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wxGetApp().plater()->get_view3D_canvas3D()->post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Selection::center_plate(const int plate_idx) {
|
void Selection::center_plate(const int plate_idx) {
|
||||||
|
|||||||
@@ -350,9 +350,13 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path,
|
|||||||
if (should_open_in_external_browser()) {
|
if (should_open_in_external_browser()) {
|
||||||
wxLaunchDefaultBrowser(url);
|
wxLaunchDefaultBrowser(url);
|
||||||
} else {
|
} else {
|
||||||
const auto mainframe = GUI::wxGetApp().mainframe;
|
// The tab switch queues a reload of the printer's home page, so switch synchronously
|
||||||
mainframe->request_select_tab(TAB_ID_MONITOR);
|
// first and queue the import page after it.
|
||||||
mainframe->load_printer_url(url);
|
GUI::wxGetApp().CallAfter([url = wxString::FromUTF8(url)] {
|
||||||
|
const auto mainframe = GUI::wxGetApp().mainframe;
|
||||||
|
mainframe->select_tab(TAB_ID_MONITOR);
|
||||||
|
mainframe->load_printer_url(url);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -183,52 +183,47 @@ static const char* category_name(AuditEventCategory category)
|
|||||||
// Path safety
|
// Path safety
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boost::filesystem::path& allowed_root)
|
||||||
|
|
||||||
// Canonicalize a path for containment comparison. weakly_canonical resolves symlinks but does
|
|
||||||
// NOT require the path to exist — it canonicalizes the prefix that exists and appends the
|
|
||||||
// non-existing tail lexically.
|
|
||||||
boost::filesystem::path canonical_for_comparison(const boost::filesystem::path& path)
|
|
||||||
{
|
{
|
||||||
namespace fs = boost::filesystem;
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
fs::path canon = fs::weakly_canonical(path, ec);
|
|
||||||
|
// Canonicalize both paths. weakly_canonical resolves symlinks but does
|
||||||
|
// NOT require the path to exist — it canonicalizes the prefix that exists
|
||||||
|
// and appends the non-existing tail lexically.
|
||||||
|
fs::path canon_candidate = fs::weakly_canonical(candidate, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
// Fall back to lexically_normal + absolute
|
// Fall back to lexically_normal + absolute
|
||||||
canon = fs::absolute(path, ec).lexically_normal();
|
canon_candidate = fs::absolute(candidate, ec).lexically_normal();
|
||||||
if (ec)
|
if (ec)
|
||||||
canon = path;
|
canon_candidate = candidate;
|
||||||
}
|
}
|
||||||
return canon;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool same_path_component(const boost::filesystem::path& lhs, const boost::filesystem::path& rhs)
|
fs::path canon_root = fs::weakly_canonical(allowed_root, ec);
|
||||||
{
|
if (ec) {
|
||||||
|
canon_root = fs::absolute(allowed_root, ec).lexically_normal();
|
||||||
|
if (ec)
|
||||||
|
canon_root = allowed_root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component-wise comparison: the root must be a prefix of candidate,
|
||||||
|
// and the next component must not be ".." or missing.
|
||||||
|
auto cand_it = canon_candidate.begin();
|
||||||
|
auto cand_end = canon_candidate.end();
|
||||||
|
auto root_it = canon_root.begin();
|
||||||
|
auto root_end = canon_root.end();
|
||||||
|
|
||||||
|
const auto same_component = [](const fs::path& lhs, const fs::path& rhs) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return boost::algorithm::iequals(lhs.native(), rhs.native());
|
return boost::algorithm::iequals(lhs.native(), rhs.native());
|
||||||
#else
|
#else
|
||||||
return lhs == rhs;
|
return lhs == rhs;
|
||||||
#endif
|
#endif
|
||||||
}
|
};
|
||||||
|
|
||||||
// Component-wise containment: the whole of canonical_root must be a prefix of
|
|
||||||
// canonical_candidate, and the remaining components must not traverse upward. Both arguments are
|
|
||||||
// expected to come from canonical_for_comparison(). On a match, suffix receives the candidate's
|
|
||||||
// components below the root (empty when the two paths are equal).
|
|
||||||
bool suffix_inside_root(const boost::filesystem::path& canonical_candidate,
|
|
||||||
const boost::filesystem::path& canonical_root,
|
|
||||||
boost::filesystem::path& suffix)
|
|
||||||
{
|
|
||||||
suffix.clear();
|
|
||||||
|
|
||||||
auto cand_it = canonical_candidate.begin();
|
|
||||||
auto cand_end = canonical_candidate.end();
|
|
||||||
auto root_it = canonical_root.begin();
|
|
||||||
auto root_end = canonical_root.end();
|
|
||||||
|
|
||||||
// Consume matching components
|
// Consume matching components
|
||||||
while (root_it != root_end && cand_it != cand_end && same_path_component(*root_it, *cand_it)) {
|
while (root_it != root_end && cand_it != cand_end && same_component(*root_it, *cand_it)) {
|
||||||
++root_it;
|
++root_it;
|
||||||
++cand_it;
|
++cand_it;
|
||||||
}
|
}
|
||||||
@@ -243,19 +238,9 @@ bool suffix_inside_root(const boost::filesystem::path& canonical_candidate,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; cand_it != cand_end; ++cand_it)
|
|
||||||
suffix /= *cand_it;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boost::filesystem::path& allowed_root)
|
|
||||||
{
|
|
||||||
boost::filesystem::path suffix;
|
|
||||||
return suffix_inside_root(canonical_for_comparison(candidate), canonical_for_comparison(allowed_root), suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ScopedPluginAuditContext
|
// ScopedPluginAuditContext
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -368,16 +353,18 @@ void PluginAuditManager::add_denied_filename(const std::string& filename)
|
|||||||
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
||||||
{
|
{
|
||||||
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
||||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||||
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
||||||
// string comparison each. Single-sourced here so install_hook() and the tests seed from the
|
// string comparison each. Keep the plugin install-state sidecar here too: it controls which
|
||||||
// same list and cannot drift apart.
|
// packages load on startup and must remain host-managed. Single-sourced so install_hook() and
|
||||||
|
// the tests seed from the same list and cannot drift apart.
|
||||||
return {
|
return {
|
||||||
SLIC3R_APP_KEY ".conf",
|
SLIC3R_APP_KEY ".conf",
|
||||||
GCODEVIEWER_APP_KEY ".conf",
|
GCODEVIEWER_APP_KEY ".conf",
|
||||||
SLIC3R_APP_KEY ".ini",
|
SLIC3R_APP_KEY ".ini",
|
||||||
GCODEVIEWER_APP_KEY ".ini",
|
GCODEVIEWER_APP_KEY ".ini",
|
||||||
secret_constants::USER_SECRET_FILENAME,
|
secret_constants::USER_SECRET_FILENAME,
|
||||||
|
".install_state.json",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,8 +412,7 @@ std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
|
|||||||
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
|
// 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
|
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
|
||||||
// granted as a read-only allowed root). Match as whole path components, not substrings, so
|
// granted as a read-only allowed root). Match as whole path components, not substrings, so
|
||||||
// imports such as numpy/__config__.py and stdlib configparser.py remain usable. Inside
|
// imports such as numpy/__config__.py and stdlib configparser.py remain usable.
|
||||||
// data_dir() they match below that root only; see is_denied_path_keyword().
|
|
||||||
return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"};
|
return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,29 +431,19 @@ bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& c
|
|||||||
{
|
{
|
||||||
namespace fs = boost::filesystem;
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
const fs::path canon = canonical_for_comparison(candidate);
|
boost::system::error_code ec;
|
||||||
|
fs::path canon = fs::weakly_canonical(candidate, ec);
|
||||||
// The app's data folder can itself sit below a component named like one of the keywords
|
if (ec) {
|
||||||
// without being one: Flatpak keeps it under <home>/.var/app/<app-id>/config/OrcaSlicer, and
|
canon = fs::absolute(candidate, ec).lexically_normal();
|
||||||
// the sandbox's home layout is not ours to change. A keyword above data_dir() therefore says
|
if (ec)
|
||||||
// nothing about what a plugin may reach inside it, so inside data_dir() the keywords apply to
|
canon = candidate;
|
||||||
// the suffix below data_dir() only. Everywhere else -- resources_dir() with the bundled
|
}
|
||||||
// certificates, a plugin's own folders, arbitrary files -- the whole path is still checked,
|
|
||||||
// and so is a keyword under data_dir() itself.
|
|
||||||
fs::path canonical_data_dir;
|
|
||||||
if (!data_dir().empty())
|
|
||||||
canonical_data_dir = canonical_for_comparison(data_dir());
|
|
||||||
|
|
||||||
fs::path scope = canon;
|
|
||||||
fs::path suffix;
|
|
||||||
if (!canonical_data_dir.empty() && suffix_inside_root(canon, canonical_data_dir, suffix))
|
|
||||||
scope = suffix;
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
if (m_denied_path_keywords.empty())
|
if (m_denied_path_keywords.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
for (const auto& component : scope) {
|
for (const auto& component : canon) {
|
||||||
std::string name = component.string();
|
std::string name = component.string();
|
||||||
if (name.empty())
|
if (name.empty())
|
||||||
continue;
|
continue;
|
||||||
@@ -524,6 +500,12 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
|||||||
candidate = absolute_candidate;
|
candidate = absolute_candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Importing Python modules reads their source and may inspect package directories. These
|
||||||
|
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
|
||||||
|
// and exclude third-party packages from the stdlib roots.
|
||||||
|
if (!is_write && is_stdlib_path(candidate))
|
||||||
|
return {true, ""};
|
||||||
|
|
||||||
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
// 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
|
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
||||||
// path is physically inside it.
|
// path is physically inside it.
|
||||||
@@ -1041,6 +1023,22 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, b
|
|||||||
|
|
||||||
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||||
|
|
||||||
|
static const char* audit_category_name(AuditEventCategory category)
|
||||||
|
{
|
||||||
|
switch (category) {
|
||||||
|
case AuditEventCategory::FsRead: return "FsRead";
|
||||||
|
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
|
||||||
|
case AuditEventCategory::FsCreate: return "FsCreate";
|
||||||
|
case AuditEventCategory::FsDelete: return "FsDelete";
|
||||||
|
case AuditEventCategory::Http: return "Http";
|
||||||
|
case AuditEventCategory::Socket: return "Socket";
|
||||||
|
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
|
||||||
|
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
|
||||||
|
case AuditEventCategory::Threading: return "Threading";
|
||||||
|
default: return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||||
// no target (by_event), which the prompt does not show as a target.
|
// no target (by_event), which the prompt does not show as a target.
|
||||||
@@ -1058,6 +1056,16 @@ bool request_permission(AuditEventCategory category, const std::string& plugin_k
|
|||||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||||
{
|
{
|
||||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||||
|
std::string targets_text = by_event ? "<none>" : "";
|
||||||
|
for (const auto& target : shown) {
|
||||||
|
if (!targets_text.empty())
|
||||||
|
targets_text += ", ";
|
||||||
|
targets_text += target;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
|
||||||
|
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
|
||||||
|
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
|
||||||
|
|
||||||
if (s_permission_prompt_for_testing)
|
if (s_permission_prompt_for_testing)
|
||||||
return s_permission_prompt_for_testing(event_name, shown);
|
return s_permission_prompt_for_testing(event_name, shown);
|
||||||
@@ -1224,6 +1232,18 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename)
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
|
||||||
|
return std::any_of(dirs.begin(), dirs.end(),
|
||||||
|
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
|
||||||
|
};
|
||||||
|
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
|
||||||
|
}
|
||||||
|
|
||||||
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||||
{
|
{
|
||||||
if (frames.empty())
|
if (frames.empty())
|
||||||
@@ -1292,7 +1312,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
|||||||
|
|
||||||
void PluginAuditManager::install_hook()
|
void PluginAuditManager::install_hook()
|
||||||
{
|
{
|
||||||
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
|
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
|
||||||
PluginAuditDetail::collect_stdlib_dirs();
|
PluginAuditDetail::collect_stdlib_dirs();
|
||||||
|
|
||||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||||
@@ -1309,8 +1329,9 @@ void PluginAuditManager::install_hook()
|
|||||||
// "cert" denied-path keyword seeded below.
|
// "cert" denied-path keyword seeded below.
|
||||||
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||||
|
|
||||||
// The user's app config and cloud credentials live directly inside data_dir(), so the
|
// The user's app config and cloud credentials live directly inside data_dir(), and plugin
|
||||||
// root just granted would otherwise expose them to any plugin. Deny them by name.
|
// install state lives beneath it, so the root just granted would otherwise expose them to
|
||||||
|
// plugins. Deny them by name.
|
||||||
//
|
//
|
||||||
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
||||||
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
||||||
@@ -1321,12 +1342,10 @@ void PluginAuditManager::install_hook()
|
|||||||
for (const auto& name : default_denied_filenames())
|
for (const auto& name : default_denied_filenames())
|
||||||
add_denied_filename(name);
|
add_denied_filename(name);
|
||||||
|
|
||||||
// Categorical denies on top of the exact-name list above: a path a plugin reaches must not
|
// Categorical denies on top of the exact-name list above: no path a plugin can reach may
|
||||||
// have a "secret", "cert"(ificate), or "conf"(ig) component below data_dir() -- or anywhere
|
// contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which
|
||||||
// in the path outside data_dir(), whose own location may legitimately contain one (Flatpak
|
// allowed root it happens to sit inside. default_denied_path_keywords() is the single
|
||||||
// keeps it under .../config/OrcaSlicer). This holds regardless of which allowed root the
|
// source of this list; the tests seed from it too.
|
||||||
// path 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())
|
for (const auto& keyword : default_denied_path_keywords())
|
||||||
add_denied_path_keyword(keyword);
|
add_denied_path_keyword(keyword);
|
||||||
|
|
||||||
|
|||||||
@@ -102,13 +102,12 @@ public:
|
|||||||
bool is_denied_filename(const boost::filesystem::path& candidate) const;
|
bool is_denied_filename(const boost::filesystem::path& candidate) const;
|
||||||
|
|
||||||
// --- denied-path-keyword registry ---
|
// --- denied-path-keyword registry ---
|
||||||
// Keywords that categorically deny a path when a component matches one case-insensitively --
|
// Keywords that categorically deny a path if ANY component matches one case-insensitively --
|
||||||
// e.g. a "secrets" subfolder, a "certificates" folder, a "conf"/"config" directory, or a
|
// e.g. a "secrets" subfolder, a "certificates" folder, a "conf"/"config" directory, or a
|
||||||
// .conf/.ini file anywhere the plugin can otherwise reach, including inside an allowed root.
|
// .conf/.ini file anywhere the plugin can otherwise reach, including inside an allowed root.
|
||||||
// The components above the app's data_dir() are not considered (see below). This is broader
|
// This is broader than the exact-name is_denied_filename registry, but it is not a substring
|
||||||
// than the exact-name is_denied_filename registry, but it is not a substring match:
|
// match: importable modules such as numpy/__config__.py, configparser.py, sysconfig.py, or
|
||||||
// importable modules such as numpy/__config__.py, configparser.py, sysconfig.py, or user
|
// user folders such as "Conference" and "Concert" are unrelated names and must stay promptable.
|
||||||
// folders such as "Conference" and "Concert" are unrelated names and must stay promptable.
|
|
||||||
void add_denied_path_keyword(const std::string& keyword);
|
void add_denied_path_keyword(const std::string& keyword);
|
||||||
|
|
||||||
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
|
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
|
||||||
@@ -116,10 +115,7 @@ public:
|
|||||||
static std::vector<std::string> default_denied_path_keywords();
|
static std::vector<std::string> default_denied_path_keywords();
|
||||||
|
|
||||||
// True when any component of candidate's (canonicalized) path matches a registered keyword,
|
// True when any component of candidate's (canonicalized) path matches a registered keyword,
|
||||||
// case-insensitively. Inside data_dir(), only the components below data_dir() are checked:
|
// case-insensitively. A registered "conf" keyword also blocks .conf/.ini file components.
|
||||||
// the app's own data folder may itself sit under a component named like a keyword (Flatpak
|
|
||||||
// keeps it under .../config/OrcaSlicer), which must not deny everything inside it. A
|
|
||||||
// registered "conf" keyword also blocks .conf/.ini file components.
|
|
||||||
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
|
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
|
||||||
|
|
||||||
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
|
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
|
||||||
@@ -177,6 +173,11 @@ private:
|
|||||||
|
|
||||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||||
|
|
||||||
|
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
|
||||||
|
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
|
||||||
|
// symlinks that can change.
|
||||||
|
static bool is_stdlib_path(const boost::filesystem::path& path);
|
||||||
|
|
||||||
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
||||||
void release_frames(std::vector<PyObject*>& frames);
|
void release_frames(std::vector<PyObject*>& frames);
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ void seed_denied_keywords()
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
|
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
|
||||||
{
|
{
|
||||||
seed_denied_names();
|
seed_denied_names();
|
||||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||||
@@ -81,6 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
|
|||||||
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
||||||
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
||||||
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
||||||
|
CHECK(mgr.is_denied_filename(fs::path(".install_state.json")));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("companions holding the same secrets are denied by the prefix rule")
|
SECTION("companions holding the same secrets are denied by the prefix rule")
|
||||||
@@ -136,6 +137,7 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
|||||||
|
|
||||||
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
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;
|
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
||||||
|
const fs::path install_state = fs::path(data_dir()) / "plugins" / "example" / ".install_state.json";
|
||||||
|
|
||||||
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
||||||
{
|
{
|
||||||
@@ -176,6 +178,17 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
|||||||
CHECK_FALSE(decision.allowed);
|
CHECK_FALSE(decision.allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("plugin install state is blocked for reads and writes despite the allowed root")
|
||||||
|
{
|
||||||
|
AuditDecision read_decision = mgr.check_open(install_state.string(), "r");
|
||||||
|
CHECK_FALSE(read_decision.allowed);
|
||||||
|
CHECK(read_decision.reason == "denied filename");
|
||||||
|
|
||||||
|
AuditDecision write_decision = mgr.check_open(install_state.string(), "w");
|
||||||
|
CHECK_FALSE(write_decision.allowed);
|
||||||
|
CHECK(write_decision.reason == "denied filename");
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("a traversal path resolving to the config is blocked")
|
SECTION("a traversal path resolving to the config is blocked")
|
||||||
{
|
{
|
||||||
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
||||||
@@ -288,67 +301,6 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword",
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin audit checks denied keywords below data_dir, not in the directories above it", "[PluginAudit]")
|
|
||||||
{
|
|
||||||
// Flatpak keeps the app's data folder under <home>/.var/app/<app-id>/config/OrcaSlicer, so
|
|
||||||
// data_dir() itself has a "config" component. That ancestor describes the sandbox's layout,
|
|
||||||
// not what the folder holds, and must not hard-deny every path inside it.
|
|
||||||
ScopedDataDir data_dir_guard("plugin-audit-data-dir-keyword");
|
|
||||||
const fs::path flatpak_data_dir = data_dir_guard.dir / "config" / "OrcaSlicer";
|
|
||||||
set_data_dir(flatpak_data_dir.string());
|
|
||||||
|
|
||||||
seed_denied_names();
|
|
||||||
seed_denied_keywords();
|
|
||||||
|
|
||||||
PluginAuditManager& mgr = PluginAuditManager::instance();
|
|
||||||
// Reproduce install_hook()'s grant: data_dir() is a global allowed root.
|
|
||||||
mgr.add_global_allowed_root(data_dir());
|
|
||||||
|
|
||||||
ScopedPluginAuditContext ctx("test_plugin", "");
|
|
||||||
|
|
||||||
SECTION("the root and the paths below it that carry no keyword are allowed")
|
|
||||||
{
|
|
||||||
CHECK_FALSE(mgr.is_denied_path_keyword(flatpak_data_dir / "orca_plugins" / "state.json"));
|
|
||||||
CHECK(mgr.check_open(data_dir(), "r").allowed);
|
|
||||||
CHECK(mgr.check_open((flatpak_data_dir / "orca_plugins" / "test_plugin" / "state.json").string(), "r").allowed);
|
|
||||||
CHECK(mgr.check_open((flatpak_data_dir / "orca_plugins" / "test_plugin" / "state.json").string(), "w").allowed);
|
|
||||||
CHECK(mgr.check_open((flatpak_data_dir / "system" / "profiles" / "vendor.json").string(), "r").allowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a 'secrets' component below data_dir() is still denied")
|
|
||||||
{
|
|
||||||
AuditDecision decision = mgr.check_open((flatpak_data_dir / "secrets" / "token.json").string(), "r");
|
|
||||||
CHECK_FALSE(decision.allowed);
|
|
||||||
CHECK(decision.reason == "denied path keyword");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a 'config' component below data_dir() is still denied")
|
|
||||||
{
|
|
||||||
AuditDecision decision = mgr.check_open((flatpak_data_dir / "config" / "settings.json").string(), "r");
|
|
||||||
CHECK_FALSE(decision.allowed);
|
|
||||||
CHECK(decision.reason == "denied path keyword");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("the app config below data_dir() is still denied by name")
|
|
||||||
{
|
|
||||||
AuditDecision decision = mgr.check_open((flatpak_data_dir / (SLIC3R_APP_KEY ".conf")).string(), "r");
|
|
||||||
CHECK_FALSE(decision.allowed);
|
|
||||||
CHECK(decision.reason == "denied filename");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("an unrelated resources root keeps the full-path keyword check")
|
|
||||||
{
|
|
||||||
ScopedResourcesDir resources_dir_guard("plugin-audit-data-dir-keyword-resources");
|
|
||||||
mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
|
||||||
|
|
||||||
CHECK(mgr.check_open((fs::path(resources_dir()) / "profiles" / "vendor.json").string(), "r").allowed);
|
|
||||||
|
|
||||||
AuditDecision decision = mgr.check_open((fs::path(resources_dir()) / "cert" / "slicer_base64.cer").string(), "r");
|
|
||||||
CHECK_FALSE(decision.allowed);
|
|
||||||
CHECK(decision.reason == "denied path keyword");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
|
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
|
||||||
{
|
{
|
||||||
seed_denied_names();
|
seed_denied_names();
|
||||||
@@ -1180,3 +1132,37 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][
|
|||||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
||||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
|
||||||
|
{
|
||||||
|
AuditHarness harness;
|
||||||
|
if (!harness.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
harness.add_plugin(PLUGIN);
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
PythonSide python(harness);
|
||||||
|
|
||||||
|
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
|
||||||
|
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
|
||||||
|
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
|
||||||
|
// os.py ships with every CPython installation. A site-packages path is third-party code even
|
||||||
|
// when the directory is empty: the prompt must come before the missing file is noticed.
|
||||||
|
const fs::path stdlib_file = stdlib / "os.py";
|
||||||
|
REQUIRE(fs::exists(stdlib_file));
|
||||||
|
|
||||||
|
python.globals["STDLIB_FILE"] = stdlib_file.string();
|
||||||
|
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
|
||||||
|
python.run_as(PLUGIN, R"PY(
|
||||||
|
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
|
||||||
|
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
|
||||||
|
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
|
||||||
|
)PY");
|
||||||
|
|
||||||
|
CHECK(python.str("read_outcome") == "ok");
|
||||||
|
CHECK(python.str("write_outcome") == "denied");
|
||||||
|
CHECK(python.str("site_packages_outcome") == "denied");
|
||||||
|
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
|
||||||
|
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
|
||||||
|
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
|
||||||
|
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user