diff --git a/check-linuxcnc-wasm-shims-source-map.sh b/check-linuxcnc-wasm-shims-source-map.sh index b1f54c3..fbb5214 100755 --- a/check-linuxcnc-wasm-shims-source-map.sh +++ b/check-linuxcnc-wasm-shims-source-map.sh @@ -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", diff --git a/core/wasm_shims/pythonplugin/python_plugin.cc b/core/wasm_shims/pythonplugin/python_plugin.cc index 72b9ddc..62174ee 100644 --- a/core/wasm_shims/pythonplugin/python_plugin.cc +++ b/core/wasm_shims/pythonplugin/python_plugin.cc @@ -1,7 +1,12 @@ #include "pythonplugin/python_plugin.hh" +#include #include #include +#include +#include +#include +#include // 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(); } diff --git a/core/wasm_shims/pythonplugin/python_plugin_probe_main.cc b/core/wasm_shims/pythonplugin/python_plugin_probe_main.cc index 1a17dcb..e599854 100644 --- a/core/wasm_shims/pythonplugin/python_plugin_probe_main.cc +++ b/core/wasm_shims/pythonplugin/python_plugin_probe_main.cc @@ -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); diff --git a/docs/linuxcnc-wasm-shims-source-map.md b/docs/linuxcnc-wasm-shims-source-map.md index 39a518e..6129e8a 100644 --- a/docs/linuxcnc-wasm-shims-source-map.md +++ b/docs/linuxcnc-wasm-shims-source-map.md @@ -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