按源保护 PythonPlugin TOPLEVEL 路径边界

This commit is contained in:
cnc
2026-06-06 05:26:33 +08:00
parent 66ea34a76d
commit 4adcd82b74
4 changed files with 113 additions and 2 deletions

View File

@@ -92,10 +92,16 @@ grep -F 'return vsnprintf(buffer, size, fmt, args);' \
"$linuxcnc_root/src/rtapi/uspace_common.h" >/dev/null
grep -F 'class PythonPlugin' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
grep -F 'PLUGIN_PYTHON_NOT_INITIALIZED = -12,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
grep -F 'PLUGIN_BAD_PATH = -5,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
grep -F 'PLUGIN_NO_CALLABLE = 1,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
grep -F 'PLUGIN_EXCEPTION = 2' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
grep -F 'PythonPlugin::instantiate' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'if (status < PLUGIN_OK)' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'if (auto inistring = inifile.findString("TOPLEVEL", section)) {' \
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'if (realpath(toplevel, real_path) == NULL) {' \
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'status = PLUGIN_BAD_PATH;' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'retval = bp::exec_file(cmd, main_namespace, main_namespace);' \
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
grep -F 'retval = bp::exec(cmd, main_namespace, main_namespace);' \
@@ -317,6 +323,9 @@ for forbidden in [
raise SystemExit(f"PythonPlugin wasm shim must not execute Python: {forbidden}")
for needle in [
"python disabled in wasm-safe probe",
'find_ini_value(iniFilename, section, "TOPLEVEL", toplevel)',
"realpath(toplevel.c_str(), resolved_path) == nullptr",
"status = PLUGIN_BAD_PATH;",
"status = PLUGIN_PYTHON_NOT_INITIALIZED;",
"return PLUGIN_NO_CALLABLE;",
"status = PLUGIN_EXCEPTION;",
@@ -327,6 +336,7 @@ for needle in [
for needle in [
"PLUGIN_NO_SECTION",
"PLUGIN_BAD_INIFILE",
"PLUGIN_BAD_PATH",
"PLUGIN_PYTHON_NOT_INITIALIZED",
"PLUGIN_NO_CALLABLE",
"PLUGIN_EXCEPTION",
@@ -407,6 +417,7 @@ for phrase in [
"must not execute `bp::exec`, `bp::exec_file`, or",
"inactive-DB return values",
"negative-status short-circuit behavior",
"preserve the `TOPLEVEL` missing-path `PLUGIN_BAD_PATH` edge",
"LinuxCNC `rcs_print_error` split entry points",
"without native print routing",
"LinuxCNC userspace `rtapi_snprintf()` formatting",

View File

@@ -1,7 +1,12 @@
#include "pythonplugin/python_plugin.hh"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits.h>
#include <string>
#include <unistd.h>
// LinuxCNC source basis: src/emc/pythonplugin/python_plugin.cc provides the
// native PythonPlugin implementation called by RS274 Python/remap paths.
@@ -11,6 +16,61 @@ __attribute__((weak)) std::string handle_pyerror() {
PythonPlugin *python_plugin = nullptr;
namespace {
std::string trim_ini_value(std::string value) {
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
});
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
}).base();
if (first >= last) {
return {};
}
return std::string(first, last);
}
bool find_ini_value(const char *iniFilename,
const char *section,
const char *key,
std::string &value) {
FILE *file = std::fopen(iniFilename, "r");
if (!file) {
return false;
}
bool in_section = false;
char line[PATH_MAX + 256];
while (std::fgets(line, sizeof(line), file)) {
std::string text = trim_ini_value(line);
if (text.empty() || text[0] == ';' || text[0] == '#') {
continue;
}
if (text.front() == '[' && text.back() == ']') {
in_section = text.substr(1, text.size() - 2) == section;
continue;
}
if (!in_section) {
continue;
}
const auto equals = text.find('=');
if (equals == std::string::npos) {
continue;
}
if (trim_ini_value(text.substr(0, equals)) == key) {
value = trim_ini_value(text.substr(equals + 1));
std::fclose(file);
return true;
}
}
std::fclose(file);
return false;
}
} // namespace
PythonPlugin *PythonPlugin::instantiate(struct _inittab *) {
static PythonPlugin plugin;
python_plugin = &plugin;
@@ -42,6 +102,19 @@ int PythonPlugin::configure(const char *iniFilename, const char *section) {
}
std::fclose(file);
std::string toplevel;
if (find_ini_value(iniFilename, section, "TOPLEVEL", toplevel)) {
char resolved_path[PATH_MAX];
// LinuxCNC source basis: python_plugin.cc configure() expands and
// resolves [PYTHON] TOPLEVEL before initialize(); a missing path
// returns PLUGIN_BAD_PATH before Python runtime status is considered.
if (realpath(toplevel.c_str(), resolved_path) == nullptr) {
error_msg = "bad path";
status = PLUGIN_BAD_PATH;
return status;
}
}
return initialize();
}

View File

@@ -60,6 +60,32 @@ int main() {
std::fputs("[PYTHON]\n", file);
std::fclose(file);
}
const char *bad_path_ini = "/tmp/cnc_sim_python_plugin_probe_bad_path.ini";
{
FILE *file = std::fopen(bad_path_ini, "w");
if (!file) {
std::remove(ini);
return 33;
}
std::fputs("[PYTHON]\nTOPLEVEL = /tmp/cnc_sim_missing_python_toplevel.py\n", file);
std::fclose(file);
}
if (plugin->configure(bad_path_ini, "PYTHON") != PLUGIN_BAD_PATH) {
std::remove(ini);
std::remove(bad_path_ini);
return 34;
}
if (plugin->usable()) {
std::remove(ini);
std::remove(bad_path_ini);
return 35;
}
if (plugin->last_errmsg() != "bad path") {
std::remove(ini);
std::remove(bad_path_ini);
return 36;
}
std::remove(bad_path_ini);
setenv("INI_FILE_NAME", ini, 1);
if (plugin->configure(nullptr, "PYTHON") != PLUGIN_PYTHON_NOT_INITIALIZED) {
std::remove(ini);

View File

@@ -61,8 +61,9 @@ only; it must not add CNC behavior and must not expand the temporary smoke parse
`python_plugin.cc` link expectations; it must not call into CPython runtime.
- `core/wasm_shims/pythonplugin/python_plugin.cc` must preserve LinuxCNC
`PythonPlugin` status edges for `PLUGIN_NO_SECTION`, `PLUGIN_BAD_INIFILE`,
`PLUGIN_PYTHON_NOT_INITIALIZED`, `PLUGIN_NO_CALLABLE`, and
`PLUGIN_EXCEPTION` while keeping Python execution disabled.
`PLUGIN_BAD_PATH`, `PLUGIN_PYTHON_NOT_INITIALIZED`, `PLUGIN_NO_CALLABLE`,
and `PLUGIN_EXCEPTION` while keeping Python execution disabled.
- The PythonPlugin shim must preserve the `TOPLEVEL` missing-path `PLUGIN_BAD_PATH` edge from `src/emc/pythonplugin/python_plugin.cc` before reporting the browser-safe disabled Python runtime status.
- The PythonPlugin shim must not execute `bp::exec`, `bp::exec_file`, or real
Python callable dispatch; `PyObject_Call` is an inert C API symbol only.
Browser Python execution remains blocked until implemented as a source-backed