按源保护 PythonPlugin 路径列表边界
This commit is contained in:
@@ -93,6 +93,8 @@ grep -F 'return vsnprintf(buffer, size, fmt, args);' \
|
|||||||
grep -F 'class PythonPlugin' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/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_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_BAD_PATH = -5,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
|
||||||
|
grep -F 'PLUGIN_EXCEPTION_DURING_PATH_PREPEND = -9,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null
|
||||||
|
grep -F 'PLUGIN_EXCEPTION_DURING_PATH_APPEND = -10,' "$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_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 '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 'PythonPlugin::instantiate' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
@@ -102,6 +104,17 @@ grep -F 'if (auto inistring = inifile.findString("TOPLEVEL", section)) {' \
|
|||||||
grep -F 'if (realpath(toplevel, real_path) == NULL) {' \
|
grep -F 'if (realpath(toplevel, real_path) == NULL) {' \
|
||||||
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/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 'status = PLUGIN_BAD_PATH;' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
|
grep -F 'while (auto inistring = inifile.findString(n, "PATH_PREPEND", "PYTHON")) {' \
|
||||||
|
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
|
grep -F 'while (auto inistring = inifile.findString(n, "PATH_APPEND", "PYTHON")) {' \
|
||||||
|
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
|
grep -F 'status = PLUGIN_EXCEPTION_DURING_PATH_PREPEND;' \
|
||||||
|
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
|
grep -F 'status = PLUGIN_EXCEPTION_DURING_PATH_APPEND;' \
|
||||||
|
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
|
grep -F 'int IniFile::tildeExpand(const std::string &path, std::string &result)' \
|
||||||
|
"$linuxcnc_root/src/emc/ini/inifile.cc" >/dev/null
|
||||||
|
grep -F "if(!home)" "$linuxcnc_root/src/emc/ini/inifile.cc" >/dev/null
|
||||||
grep -F 'retval = bp::exec_file(cmd, main_namespace, main_namespace);' \
|
grep -F 'retval = bp::exec_file(cmd, main_namespace, main_namespace);' \
|
||||||
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
"$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null
|
||||||
grep -F 'retval = bp::exec(cmd, main_namespace, main_namespace);' \
|
grep -F 'retval = bp::exec(cmd, main_namespace, main_namespace);' \
|
||||||
@@ -195,6 +208,7 @@ expected_shims = {
|
|||||||
"core/wasm_shims/pythonplugin/python_plugin.cc": [
|
"core/wasm_shims/pythonplugin/python_plugin.cc": [
|
||||||
"src/emc/pythonplugin/python_plugin.cc",
|
"src/emc/pythonplugin/python_plugin.cc",
|
||||||
"src/emc/pythonplugin/python_plugin.hh",
|
"src/emc/pythonplugin/python_plugin.hh",
|
||||||
|
"src/emc/ini/inifile.cc",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
if shim_paths != list(expected_shims):
|
if shim_paths != list(expected_shims):
|
||||||
@@ -326,6 +340,10 @@ for needle in [
|
|||||||
'find_ini_value(iniFilename, section, "TOPLEVEL", toplevel)',
|
'find_ini_value(iniFilename, section, "TOPLEVEL", toplevel)',
|
||||||
"realpath(toplevel.c_str(), resolved_path) == nullptr",
|
"realpath(toplevel.c_str(), resolved_path) == nullptr",
|
||||||
"status = PLUGIN_BAD_PATH;",
|
"status = PLUGIN_BAD_PATH;",
|
||||||
|
'check_python_path_entries(iniFilename,',
|
||||||
|
"PLUGIN_EXCEPTION_DURING_PATH_PREPEND",
|
||||||
|
"PLUGIN_EXCEPTION_DURING_PATH_APPEND",
|
||||||
|
"tilde_expand_path(entry, expanded)",
|
||||||
"status = PLUGIN_PYTHON_NOT_INITIALIZED;",
|
"status = PLUGIN_PYTHON_NOT_INITIALIZED;",
|
||||||
"return PLUGIN_NO_CALLABLE;",
|
"return PLUGIN_NO_CALLABLE;",
|
||||||
"status = PLUGIN_EXCEPTION;",
|
"status = PLUGIN_EXCEPTION;",
|
||||||
@@ -337,6 +355,8 @@ for needle in [
|
|||||||
"PLUGIN_NO_SECTION",
|
"PLUGIN_NO_SECTION",
|
||||||
"PLUGIN_BAD_INIFILE",
|
"PLUGIN_BAD_INIFILE",
|
||||||
"PLUGIN_BAD_PATH",
|
"PLUGIN_BAD_PATH",
|
||||||
|
"PLUGIN_EXCEPTION_DURING_PATH_PREPEND",
|
||||||
|
"PLUGIN_EXCEPTION_DURING_PATH_APPEND",
|
||||||
"PLUGIN_PYTHON_NOT_INITIALIZED",
|
"PLUGIN_PYTHON_NOT_INITIALIZED",
|
||||||
"PLUGIN_NO_CALLABLE",
|
"PLUGIN_NO_CALLABLE",
|
||||||
"PLUGIN_EXCEPTION",
|
"PLUGIN_EXCEPTION",
|
||||||
@@ -418,6 +438,7 @@ for phrase in [
|
|||||||
"inactive-DB return values",
|
"inactive-DB return values",
|
||||||
"negative-status short-circuit behavior",
|
"negative-status short-circuit behavior",
|
||||||
"preserve the `TOPLEVEL` missing-path `PLUGIN_BAD_PATH` edge",
|
"preserve the `TOPLEVEL` missing-path `PLUGIN_BAD_PATH` edge",
|
||||||
|
"preserve `PATH_PREPEND` and `PATH_APPEND` tilde-expansion failure statuses",
|
||||||
"LinuxCNC `rcs_print_error` split entry points",
|
"LinuxCNC `rcs_print_error` split entry points",
|
||||||
"without native print routing",
|
"without native print routing",
|
||||||
"LinuxCNC userspace `rtapi_snprintf()` formatting",
|
"LinuxCNC userspace `rtapi_snprintf()` formatting",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
// LinuxCNC source basis: src/emc/pythonplugin/python_plugin.cc provides the
|
// LinuxCNC source basis: src/emc/pythonplugin/python_plugin.cc provides the
|
||||||
@@ -31,13 +32,13 @@ std::string trim_ini_value(std::string value) {
|
|||||||
return std::string(first, last);
|
return std::string(first, last);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool find_ini_value(const char *iniFilename,
|
std::vector<std::string> find_ini_values(const char *iniFilename,
|
||||||
const char *section,
|
const char *section,
|
||||||
const char *key,
|
const char *key) {
|
||||||
std::string &value) {
|
std::vector<std::string> values;
|
||||||
FILE *file = std::fopen(iniFilename, "r");
|
FILE *file = std::fopen(iniFilename, "r");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return false;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool in_section = false;
|
bool in_section = false;
|
||||||
@@ -59,14 +60,55 @@ bool find_ini_value(const char *iniFilename,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (trim_ini_value(text.substr(0, equals)) == key) {
|
if (trim_ini_value(text.substr(0, equals)) == key) {
|
||||||
value = trim_ini_value(text.substr(equals + 1));
|
values.push_back(trim_ini_value(text.substr(equals + 1)));
|
||||||
std::fclose(file);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fclose(file);
|
std::fclose(file);
|
||||||
return false;
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool find_ini_value(const char *iniFilename,
|
||||||
|
const char *section,
|
||||||
|
const char *key,
|
||||||
|
std::string &value) {
|
||||||
|
const std::vector<std::string> values = find_ini_values(iniFilename, section, key);
|
||||||
|
if (values.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = values.front();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tilde_expand_path(const std::string &path, std::string &expanded) {
|
||||||
|
// LinuxCNC source basis: IniFile::tildeExpand() copies non-"~/" paths and
|
||||||
|
// returns failure for "~/" paths when HOME is unavailable.
|
||||||
|
if (path.size() < 2 || path[0] != '~' || path[1] != '/') {
|
||||||
|
expanded = path;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const char *home = std::getenv("HOME");
|
||||||
|
if (!home) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
expanded = std::string(home) + path.substr(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int check_python_path_entries(const char *iniFilename,
|
||||||
|
const char *key,
|
||||||
|
pp_status failure_status) {
|
||||||
|
for (const std::string &entry : find_ini_values(iniFilename, "PYTHON", key)) {
|
||||||
|
std::string expanded;
|
||||||
|
// LinuxCNC source basis: python_plugin.cc configure() runs
|
||||||
|
// TildeExpansion on [PYTHON] PATH_PREPEND/PATH_APPEND before
|
||||||
|
// PyRun_SimpleString(); browser-safe probes preserve the expansion
|
||||||
|
// failure status without executing Python path mutation.
|
||||||
|
if (!tilde_expand_path(entry, expanded)) {
|
||||||
|
return failure_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PLUGIN_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -115,6 +157,21 @@ int PythonPlugin::configure(const char *iniFilename, const char *section) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status = check_python_path_entries(iniFilename,
|
||||||
|
"PATH_PREPEND",
|
||||||
|
PLUGIN_EXCEPTION_DURING_PATH_PREPEND);
|
||||||
|
if (status != PLUGIN_OK) {
|
||||||
|
error_msg = "bad path prepend";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = check_python_path_entries(iniFilename,
|
||||||
|
"PATH_APPEND",
|
||||||
|
PLUGIN_EXCEPTION_DURING_PATH_APPEND);
|
||||||
|
if (status != PLUGIN_OK) {
|
||||||
|
error_msg = "bad path append";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
return initialize();
|
return initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "pythonplugin/python_plugin.hh"
|
#include "pythonplugin/python_plugin.hh"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
@@ -86,6 +87,102 @@ int main() {
|
|||||||
return 36;
|
return 36;
|
||||||
}
|
}
|
||||||
std::remove(bad_path_ini);
|
std::remove(bad_path_ini);
|
||||||
|
const char *path_prepend_ini = "/tmp/cnc_sim_python_plugin_probe_path_prepend.ini";
|
||||||
|
{
|
||||||
|
FILE *file = std::fopen(path_prepend_ini, "w");
|
||||||
|
if (!file) {
|
||||||
|
std::remove(ini);
|
||||||
|
return 37;
|
||||||
|
}
|
||||||
|
std::fputs("[PYTHON]\nPATH_PREPEND = ~/linuxcnc-python\n", file);
|
||||||
|
std::fclose(file);
|
||||||
|
}
|
||||||
|
const char *saved_home = std::getenv("HOME");
|
||||||
|
const std::string saved_home_value = saved_home ? saved_home : "";
|
||||||
|
unsetenv("HOME");
|
||||||
|
if (plugin->configure(path_prepend_ini, "PYTHON") != PLUGIN_EXCEPTION_DURING_PATH_PREPEND) {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_prepend_ini);
|
||||||
|
return 38;
|
||||||
|
}
|
||||||
|
if (plugin->usable()) {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_prepend_ini);
|
||||||
|
return 39;
|
||||||
|
}
|
||||||
|
if (plugin->last_errmsg() != "bad path prepend") {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_prepend_ini);
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(path_prepend_ini);
|
||||||
|
const char *path_append_ini = "/tmp/cnc_sim_python_plugin_probe_path_append.ini";
|
||||||
|
{
|
||||||
|
FILE *file = std::fopen(path_append_ini, "w");
|
||||||
|
if (!file) {
|
||||||
|
std::remove(ini);
|
||||||
|
return 41;
|
||||||
|
}
|
||||||
|
std::fputs("[PYTHON]\nPATH_APPEND = ~/linuxcnc-python\n", file);
|
||||||
|
std::fclose(file);
|
||||||
|
}
|
||||||
|
unsetenv("HOME");
|
||||||
|
if (plugin->configure(path_append_ini, "PYTHON") != PLUGIN_EXCEPTION_DURING_PATH_APPEND) {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_append_ini);
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
if (plugin->usable()) {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_append_ini);
|
||||||
|
return 43;
|
||||||
|
}
|
||||||
|
if (plugin->last_errmsg() != "bad path append") {
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(path_append_ini);
|
||||||
|
return 44;
|
||||||
|
}
|
||||||
|
if (saved_home) {
|
||||||
|
setenv("HOME", saved_home_value.c_str(), 1);
|
||||||
|
}
|
||||||
|
std::remove(path_append_ini);
|
||||||
|
const char *plain_path_ini = "/tmp/cnc_sim_python_plugin_probe_plain_path.ini";
|
||||||
|
{
|
||||||
|
FILE *file = std::fopen(plain_path_ini, "w");
|
||||||
|
if (!file) {
|
||||||
|
std::remove(ini);
|
||||||
|
return 45;
|
||||||
|
}
|
||||||
|
std::fputs("[PYTHON]\nPATH_PREPEND = /tmp\nPATH_APPEND = /tmp\n", file);
|
||||||
|
std::fclose(file);
|
||||||
|
}
|
||||||
|
if (plugin->configure(plain_path_ini, "PYTHON") != PLUGIN_PYTHON_NOT_INITIALIZED) {
|
||||||
|
std::remove(ini);
|
||||||
|
std::remove(plain_path_ini);
|
||||||
|
return 46;
|
||||||
|
}
|
||||||
|
std::remove(plain_path_ini);
|
||||||
setenv("INI_FILE_NAME", ini, 1);
|
setenv("INI_FILE_NAME", ini, 1);
|
||||||
if (plugin->configure(nullptr, "PYTHON") != PLUGIN_PYTHON_NOT_INITIALIZED) {
|
if (plugin->configure(nullptr, "PYTHON") != PLUGIN_PYTHON_NOT_INITIALIZED) {
|
||||||
std::remove(ini);
|
std::remove(ini);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ only; it must not add CNC behavior and must not expand the temporary smoke parse
|
|||||||
| `core/wasm_shims/rtapi_compat.cc` | `src/rtapi/uspace_common.h`, `src/rtapi/rtapi.h` | Provides LinuxCNC userspace `rtapi_snprintf()` formatting by following the `rtapi.h` declaration and `uspace_common.h` `vsnprintf()` implementation path. |
|
| `core/wasm_shims/rtapi_compat.cc` | `src/rtapi/uspace_common.h`, `src/rtapi/rtapi.h` | Provides LinuxCNC userspace `rtapi_snprintf()` formatting by following the `rtapi.h` declaration and `uspace_common.h` `vsnprintf()` implementation path. |
|
||||||
| `core/wasm_shims/tooldata/tooldata_mmap_backend.cc` | `src/emc/tooldata/tooldata_mmap.cc` | Replaces native mmap storage with an in-memory WASM-safe backend for probes. |
|
| `core/wasm_shims/tooldata/tooldata_mmap_backend.cc` | `src/emc/tooldata/tooldata_mmap.cc` | Replaces native mmap storage with an in-memory WASM-safe backend for probes. |
|
||||||
| `core/wasm_shims/tooldata/tooldata_runtime_stubs.cc` | `src/emc/tooldata/tooldata_nml.cc`, `src/emc/tooldata/tooldata_db.cc` | Satisfies NML and external tool database entry points without native services, preserving inactive-DB return values. |
|
| `core/wasm_shims/tooldata/tooldata_runtime_stubs.cc` | `src/emc/tooldata/tooldata_nml.cc`, `src/emc/tooldata/tooldata_db.cc` | Satisfies NML and external tool database entry points without native services, preserving inactive-DB return values. |
|
||||||
| `core/wasm_shims/pythonplugin/python_plugin.cc` | `src/emc/pythonplugin/python_plugin.cc`, `src/emc/pythonplugin/python_plugin.hh` | Keeps LinuxCNC PythonPlugin API and negative-status short-circuit behavior available while Python execution is disabled for browser-safe probes. |
|
| `core/wasm_shims/pythonplugin/python_plugin.cc` | `src/emc/pythonplugin/python_plugin.cc`, `src/emc/pythonplugin/python_plugin.hh`, `src/emc/ini/inifile.cc` | Keeps LinuxCNC PythonPlugin API and negative-status short-circuit behavior available while Python execution is disabled for browser-safe probes. |
|
||||||
|
|
||||||
## Runtime Shim Boundaries
|
## Runtime Shim Boundaries
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ only; it must not add CNC behavior and must not expand the temporary smoke parse
|
|||||||
`PLUGIN_BAD_PATH`, `PLUGIN_PYTHON_NOT_INITIALIZED`, `PLUGIN_NO_CALLABLE`,
|
`PLUGIN_BAD_PATH`, `PLUGIN_PYTHON_NOT_INITIALIZED`, `PLUGIN_NO_CALLABLE`,
|
||||||
and `PLUGIN_EXCEPTION` while keeping Python execution disabled.
|
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 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 preserve `PATH_PREPEND` and `PATH_APPEND` tilde-expansion failure statuses from `src/emc/pythonplugin/python_plugin.cc` and `src/emc/ini/inifile.cc` without executing Python path mutation in browser-safe probes.
|
||||||
- The PythonPlugin shim must not execute `bp::exec`, `bp::exec_file`, or real
|
- 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.
|
Python callable dispatch; `PyObject_Call` is an inert C API symbol only.
|
||||||
Browser Python execution remains blocked until implemented as a source-backed
|
Browser Python execution remains blocked until implemented as a source-backed
|
||||||
|
|||||||
Reference in New Issue
Block a user