Path: blob/main/plugins/python/python_plugin_common.c
1532 views
/*1* SPDX-License-Identifier: ISC2*3* Copyright (c) 2019-2020 Robert Manner <[email protected]>4*5* Permission to use, copy, modify, and distribute this software for any6* purpose with or without fee is hereby granted, provided that the above7* copyright notice and this permission notice appear in all copies.8*9* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES10* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF11* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR12* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES13* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN14* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF15* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.16*/1718#include "python_plugin_common.h"19#include "sudo_python_module.h"2021#include <sudo_queue.h>22#include <sudo_conf.h>2324#include <limits.h>25#include <string.h>2627static struct _inittab * python_inittab_copy = NULL;28static size_t python_inittab_copy_len = 0;2930#ifndef PLUGIN_DIR31#define PLUGIN_DIR ""32#endif3334/* Py_FinalizeEx is new in version 3.6 */35#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 636# define Py_FinalizeEx() (Py_Finalize(), 0)37#endif3839static const char *40_lookup_value(char * const keyvalues[], const char *key)41{42debug_decl(_lookup_value, PYTHON_DEBUG_INTERNAL);43if (keyvalues == NULL)44debug_return_const_str(NULL);4546size_t keylen = strlen(key);47for (; *keyvalues != NULL; ++keyvalues) {48const char *keyvalue = *keyvalues;49if (strncmp(keyvalue, key, keylen) == 0 && keyvalue[keylen] == '=')50debug_return_const_str(keyvalue + keylen + 1);51}52debug_return_const_str(NULL);53}5455CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION56static int57_append_python_path(const char *module_dir)58{59debug_decl(_append_python_path, PYTHON_DEBUG_PLUGIN_LOAD);60int rc = -1;61PyObject *py_sys_path = PySys_GetObject("path"); // borrowed62if (py_sys_path == NULL) {63PyErr_Format(sudo_exc_SudoException, "Failed to get python 'path'");64debug_return_int(rc);65}6667sudo_debug_printf(SUDO_DEBUG_DIAG, "Extending python 'path' with '%s'\n", module_dir);6869PyObject *py_module_dir = PyUnicode_FromString(module_dir);70if (py_module_dir == NULL || PyList_Append(py_sys_path, py_module_dir) != 0) {71Py_XDECREF(py_module_dir);72debug_return_int(rc);73}74Py_DECREF(py_module_dir);7576if (sudo_debug_needed(SUDO_DEBUG_INFO)) {77char *path = py_join_str_list(py_sys_path, ":");78sudo_debug_printf(SUDO_DEBUG_INFO, "Python path became: %s\n", path);79free(path);80}8182rc = 0;83debug_return_int(rc);84}8586static PyObject *87_import_module(const char *path)88{89PyObject *module;90debug_decl(_import_module, PYTHON_DEBUG_PLUGIN_LOAD);9192sudo_debug_printf(SUDO_DEBUG_DIAG, "importing module: %s\n", path);9394char path_copy[PATH_MAX];95if (strlcpy(path_copy, path, sizeof(path_copy)) >= sizeof(path_copy))96debug_return_ptr(NULL);9798const char *module_dir = path_copy;99char *module_name = strrchr(path_copy, '/');100if (module_name == NULL) {101module_name = path_copy;102module_dir = "";103} else {104*module_name++ = '\0';105}106107size_t len = strlen(module_name);108if (len >= 3 && strcmp(".py", module_name + len - 3) == 0)109module_name[len - 3] = '\0';110111sudo_debug_printf(SUDO_DEBUG_INFO, "module_name: '%s', module_dir: '%s'\n", module_name, module_dir);112113if (_append_python_path(module_dir) < 0)114debug_return_ptr(NULL);115116module = PyImport_ImportModule(module_name);117if (module != NULL) {118PyObject *py_loaded_path = PyObject_GetAttrString(module, "__file__");119if (py_loaded_path != NULL) {120const char *loaded_path = PyUnicode_AsUTF8(py_loaded_path);121/* If path is a directory, loaded_path may be a file inside it. */122if (strncmp(loaded_path, path, strlen(path)) != 0) {123PyErr_Format(PyExc_Exception,124"module name conflict, tried to load %s, got %s",125path, loaded_path);126Py_CLEAR(module);127}128Py_DECREF(py_loaded_path);129}130}131debug_return_ptr(module);132}133134// Create a new sub-interpreter and switch to it.135static PyThreadState *136_python_plugin_new_interpreter(void)137{138debug_decl(_python_plugin_new_interpreter, PYTHON_DEBUG_INTERNAL);139if (py_ctx.interpreter_count >= INTERPRETER_MAX) {140PyErr_Format(PyExc_Exception, "Too many interpreters");141debug_return_ptr(NULL);142}143144PyThreadState *py_interpreter = Py_NewInterpreter();145if (py_interpreter != NULL) {146py_ctx.py_subinterpreters[py_ctx.interpreter_count] = py_interpreter;147++py_ctx.interpreter_count;148}149150debug_return_ptr(py_interpreter);151}152153static int154_save_inittab(void)155{156debug_decl(_save_inittab, PYTHON_DEBUG_INTERNAL);157free(python_inittab_copy); // just to be sure (it is always NULL)158159for (python_inittab_copy_len = 0;160PyImport_Inittab[python_inittab_copy_len].name != NULL;161++python_inittab_copy_len) {162}163++python_inittab_copy_len; // for the null mark164165python_inittab_copy = malloc(sizeof(struct _inittab) * python_inittab_copy_len);166if (python_inittab_copy == NULL) {167debug_return_int(SUDO_RC_ERROR);168}169170memcpy(python_inittab_copy, PyImport_Inittab, python_inittab_copy_len * sizeof(struct _inittab));171debug_return_int(SUDO_RC_OK);172}173174static void175_restore_inittab(void)176{177debug_decl(_restore_inittab, PYTHON_DEBUG_INTERNAL);178179if (python_inittab_copy != NULL)180memcpy(PyImport_Inittab, python_inittab_copy, python_inittab_copy_len * sizeof(struct _inittab));181182free(python_inittab_copy);183python_inittab_copy = NULL;184python_inittab_copy_len = 0;185debug_return;186}187188static void189python_plugin_handle_plugin_error_exception(PyObject **py_result, struct PluginContext *plugin_ctx)190{191debug_decl(python_plugin_handle_plugin_error_exception, PYTHON_DEBUG_INTERNAL);192193free(plugin_ctx->callback_error);194plugin_ctx->callback_error = NULL;195196if (PyErr_Occurred()) {197int rc = SUDO_RC_ERROR;198if (PyErr_ExceptionMatches(sudo_exc_PluginReject)) {199rc = SUDO_RC_REJECT;200} else if (!PyErr_ExceptionMatches(sudo_exc_PluginError)) {201debug_return;202}203204if (py_result != NULL) {205Py_CLEAR(*py_result);206*py_result = PyLong_FromLong(rc);207}208209PyObject *py_type = NULL, *py_message = NULL, *py_traceback = NULL;210PyErr_Fetch(&py_type, &py_message, &py_traceback);211212char *message = py_message ? py_create_string_rep(py_message) : NULL;213sudo_debug_printf(SUDO_DEBUG_INFO, "received sudo.PluginError exception with message '%s'",214message == NULL ? "(null)" : message);215216plugin_ctx->callback_error = message;217218Py_CLEAR(py_type);219Py_CLEAR(py_message);220Py_CLEAR(py_traceback);221}222223debug_return;224}225226int227python_plugin_construct_custom(struct PluginContext *plugin_ctx, PyObject *py_kwargs)228{229debug_decl(python_plugin_construct_custom, PYTHON_DEBUG_PLUGIN_LOAD);230int rc = SUDO_RC_ERROR;231PyObject *py_args = PyTuple_New(0);232233if (py_args == NULL)234goto cleanup;235236py_debug_python_call(python_plugin_name(plugin_ctx), "__init__",237py_args, py_kwargs, PYTHON_DEBUG_PY_CALLS);238239plugin_ctx->py_instance = PyObject_Call(plugin_ctx->py_class, py_args, py_kwargs);240python_plugin_handle_plugin_error_exception(NULL, plugin_ctx);241242py_debug_python_result(python_plugin_name(plugin_ctx), "__init__",243plugin_ctx->py_instance, PYTHON_DEBUG_PY_CALLS);244245if (plugin_ctx->py_instance)246rc = SUDO_RC_OK;247248cleanup:249if (PyErr_Occurred()) {250py_log_last_error("Failed to construct plugin instance");251Py_CLEAR(plugin_ctx->py_instance);252rc = SUDO_RC_ERROR;253}254255Py_XDECREF(py_args);256debug_return_int(rc);257}258259PyObject *260python_plugin_construct_args(unsigned int version,261char *const settings[], char *const user_info[],262char *const user_env[], char *const plugin_options[])263{264PyObject *py_settings = NULL;265PyObject *py_user_info = NULL;266PyObject *py_user_env = NULL;267PyObject *py_plugin_options = NULL;268PyObject *py_version = NULL;269PyObject *py_kwargs = NULL;270271if ((py_settings = py_str_array_to_tuple(settings)) == NULL ||272(py_user_info = py_str_array_to_tuple(user_info)) == NULL ||273(py_user_env = py_str_array_to_tuple(user_env)) == NULL ||274(py_plugin_options = py_str_array_to_tuple(plugin_options)) == NULL ||275(py_version = py_create_version(version)) == NULL ||276(py_kwargs = PyDict_New()) == NULL ||277PyDict_SetItemString(py_kwargs, "version", py_version) != 0 ||278PyDict_SetItemString(py_kwargs, "settings", py_settings) != 0 ||279PyDict_SetItemString(py_kwargs, "user_env", py_user_env) != 0 ||280PyDict_SetItemString(py_kwargs, "user_info", py_user_info) != 0 ||281PyDict_SetItemString(py_kwargs, "plugin_options", py_plugin_options) != 0)282{283Py_CLEAR(py_kwargs);284}285286Py_CLEAR(py_settings);287Py_CLEAR(py_user_info);288Py_CLEAR(py_user_env);289Py_CLEAR(py_plugin_options);290Py_CLEAR(py_version);291return py_kwargs;292}293294int295python_plugin_construct(struct PluginContext *plugin_ctx, unsigned int version,296char *const settings[], char *const user_info[],297char *const user_env[], char *const plugin_options[])298{299debug_decl(python_plugin_construct, PYTHON_DEBUG_PLUGIN_LOAD);300301int rc = SUDO_RC_ERROR;302PyObject *py_kwargs = python_plugin_construct_args(303version, settings, user_info, user_env, plugin_options);304305if (py_kwargs == NULL) {306py_log_last_error("Failed to construct plugin instance");307} else {308rc = python_plugin_construct_custom(plugin_ctx, py_kwargs);309}310311Py_CLEAR(py_kwargs);312313debug_return_int(rc);314}315316int317python_plugin_register_logging(sudo_conv_t conversation,318sudo_printf_t sudo_printf,319char * const settings[])320{321debug_decl(python_plugin_register_logging, PYTHON_DEBUG_INTERNAL);322323int rc = SUDO_RC_ERROR;324if (conversation != NULL)325py_ctx.sudo_conv = conversation;326327if (sudo_printf)328py_ctx.sudo_log = sudo_printf;329330struct sudo_conf_debug_file_list debug_files = TAILQ_HEAD_INITIALIZER(debug_files);331struct sudo_conf_debug_file_list *debug_files_ptr = &debug_files;332333const char *plugin_path = _lookup_value(settings, "plugin_path");334if (plugin_path == NULL)335plugin_path = "python_plugin.so";336337const char *debug_flags = _lookup_value(settings, "debug_flags");338339if (debug_flags == NULL) { // the group plugin does not have this information, so try to look it up340debug_files_ptr = sudo_conf_debug_files(plugin_path);341} else {342if (!python_debug_parse_flags(&debug_files, debug_flags))343goto cleanup;344}345346if (debug_files_ptr != NULL) {347if (!python_debug_register(plugin_path, debug_files_ptr))348goto cleanup;349}350351rc = SUDO_RC_OK;352353cleanup:354debug_return_int(rc);355}356357CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION358static int359_python_plugin_register_plugin_in_py_ctx(void)360{361debug_decl(_python_plugin_register_plugin_in_py_ctx, PYTHON_DEBUG_PLUGIN_LOAD);362363if (!Py_IsInitialized()) {364// Disable environment variables effecting the python interpreter365// This is important since we are running code here as root, the366// user should not be able to alter what is running any how.367#if (PY_MAJOR_VERSION > 3) || (PY_MINOR_VERSION >= 8)368PyStatus status;369PyPreConfig preconfig;370PyConfig config;371372PyPreConfig_InitPythonConfig(&preconfig);373preconfig.isolated = 1;374preconfig.use_environment = 0;375status = Py_PreInitialize(&preconfig);376if (PyStatus_Exception(status))377debug_return_int(SUDO_RC_ERROR);378379/* Inittab changes happen after pre-init but before init. */380if (_save_inittab() != SUDO_RC_OK)381debug_return_int(SUDO_RC_ERROR);382PyImport_AppendInittab("sudo", sudo_module_init);383384PyConfig_InitPythonConfig(&config);385config.isolated = 1;386status = Py_InitializeFromConfig(&config);387PyConfig_Clear(&config);388if (PyStatus_Exception(status))389debug_return_int(SUDO_RC_ERROR);390#else391Py_IgnoreEnvironmentFlag = 1;392Py_IsolatedFlag = 1;393Py_NoUserSiteDirectory = 1;394395if (_save_inittab() != SUDO_RC_OK)396debug_return_int(SUDO_RC_ERROR);397PyImport_AppendInittab("sudo", sudo_module_init);398Py_InitializeEx(0);399#endif400py_ctx.py_main_interpreter = PyThreadState_Get();401402// This ensures we import "sudo" module in the main interpreter,403// each subinterpreter will have a shallow copy.404// (This makes the C sudo module able to eg. import other modules.)405PyObject *py_sudo = NULL;406if ((py_sudo = PyImport_ImportModule("sudo")) == NULL) {407debug_return_int(SUDO_RC_ERROR);408}409Py_CLEAR(py_sudo);410} else {411PyThreadState_Swap(py_ctx.py_main_interpreter);412}413414debug_return_int(SUDO_RC_OK);415}416417static int418_python_plugin_set_path(struct PluginContext *plugin_ctx, const char *path)419{420if (path == NULL) {421py_sudo_log(SUDO_CONV_ERROR_MSG, "No python module path is specified. "422"Use 'ModulePath' plugin config option in 'sudo.conf'\n");423return SUDO_RC_ERROR;424}425426if (*path == '/') { // absolute path427plugin_ctx->plugin_path = strdup(path);428} else {429if (asprintf(&plugin_ctx->plugin_path, PLUGIN_DIR "/python/%s", path) < 0)430plugin_ctx->plugin_path = NULL;431}432433if (plugin_ctx->plugin_path == NULL) {434py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to allocate memory");435return SUDO_RC_ERROR;436}437438return SUDO_RC_OK;439}440441/* Returns the list of sudo.Plugins in a module */442static PyObject *443_python_plugin_class_list(PyObject *py_module) {444PyObject *py_module_dict = PyModule_GetDict(py_module); // Note: borrowed445PyObject *key, *value; // Note: borrowed446Py_ssize_t pos = 0;447PyObject *py_plugin_list = PyList_New(0);448449while (PyDict_Next(py_module_dict, &pos, &key, &value)) {450if (PyObject_IsSubclass(value, (PyObject *)sudo_type_Plugin) == 1) {451if (PyList_Append(py_plugin_list, key) != 0)452goto cleanup;453} else {454PyErr_Clear();455}456}457458cleanup:459if (PyErr_Occurred()) {460Py_CLEAR(py_plugin_list);461}462return py_plugin_list;463}464465/* Gets a sudo.Plugin class from the specified module. The argument "plugin_class"466* can be NULL in which case it loads the one and only "sudo.Plugin" present467* in the module (if so), or displays helpful error message. */468static PyObject *469_python_plugin_get_class(const char *plugin_path, PyObject *py_module, const char *plugin_class)470{471debug_decl(_python_plugin_get_class, PYTHON_DEBUG_PLUGIN_LOAD);472PyObject *py_plugin_list = NULL, *py_class = NULL;473474if (plugin_class == NULL) {475py_plugin_list = _python_plugin_class_list(py_module);476if (py_plugin_list == NULL) {477goto cleanup;478}479480if (PyList_Size(py_plugin_list) == 1) {481PyObject *py_plugin_name = PyList_GetItem(py_plugin_list, 0); // Note: borrowed482plugin_class = PyUnicode_AsUTF8(py_plugin_name);483}484}485486if (plugin_class == NULL) {487py_sudo_log(SUDO_CONV_ERROR_MSG, "No plugin class is specified for python module '%s'. "488"Use 'ClassName' configuration option in 'sudo.conf'\n", plugin_path);489if (py_plugin_list != NULL) {490/* Sorting the plugin list makes regress test output consistent. */491PyObject *py_obj = PyObject_CallMethod(py_plugin_list, "sort", "");492Py_CLEAR(py_obj);493char *possible_plugins = py_join_str_list(py_plugin_list, ", ");494if (possible_plugins != NULL) {495py_sudo_log(SUDO_CONV_ERROR_MSG, "Possible plugins: %s\n", possible_plugins);496free(possible_plugins);497}498}499goto cleanup;500}501502sudo_debug_printf(SUDO_DEBUG_DEBUG, "Using plugin class '%s'", plugin_class);503py_class = PyObject_GetAttrString(py_module, plugin_class);504if (py_class == NULL) {505py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to find plugin class '%s'\n", plugin_class);506PyErr_Clear();507goto cleanup;508}509510if (!PyObject_IsSubclass(py_class, (PyObject *)sudo_type_Plugin)) {511py_sudo_log(SUDO_CONV_ERROR_MSG, "Plugin class '%s' does not inherit from 'sudo.Plugin'\n", plugin_class);512Py_CLEAR(py_class);513goto cleanup;514}515516cleanup:517Py_CLEAR(py_plugin_list);518debug_return_ptr(py_class);519}520521int522python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options[],523unsigned int version)524{525debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD);526527int rc = SUDO_RC_ERROR;528529if (_python_plugin_register_plugin_in_py_ctx() != SUDO_RC_OK)530goto cleanup;531532plugin_ctx->sudo_api_version = version;533534plugin_ctx->py_interpreter = _python_plugin_new_interpreter();535if (plugin_ctx->py_interpreter == NULL) {536goto cleanup;537}538539if (sudo_module_set_default_loghandler() != SUDO_RC_OK) {540goto cleanup;541}542543if (_python_plugin_set_path(plugin_ctx, _lookup_value(plugin_options, "ModulePath")) != SUDO_RC_OK) {544goto cleanup;545}546547sudo_debug_printf(SUDO_DEBUG_DEBUG, "Loading python module from path '%s'", plugin_ctx->plugin_path);548plugin_ctx->py_module = _import_module(plugin_ctx->plugin_path);549if (plugin_ctx->py_module == NULL) {550goto cleanup;551}552553plugin_ctx->py_class = _python_plugin_get_class(plugin_ctx->plugin_path, plugin_ctx->py_module,554_lookup_value(plugin_options, "ClassName"));555if (plugin_ctx->py_class == NULL) {556goto cleanup;557}558559rc = SUDO_RC_OK;560561cleanup:562if (plugin_ctx->py_class == NULL) {563py_log_last_error("Failed during loading plugin class");564rc = SUDO_RC_ERROR;565}566567debug_return_int(rc);568}569570void571python_plugin_deinit(struct PluginContext *plugin_ctx)572{573debug_decl(python_plugin_deinit, PYTHON_DEBUG_PLUGIN_LOAD);574sudo_debug_printf(SUDO_DEBUG_DIAG, "Deinit was called for a python plugin\n");575576Py_CLEAR(plugin_ctx->py_instance);577Py_CLEAR(plugin_ctx->py_class);578Py_CLEAR(plugin_ctx->py_module);579580// Note: we are preserving the interpreters here until the unlink because581// of bugs like (strptime does not work after python interpreter reinit):582// https://bugs.python.org/issue27400583// These potentially effect a lot more python functions, simply because584// it is a rare tested scenario.585586free(plugin_ctx->callback_error);587free(plugin_ctx->plugin_path);588memset(plugin_ctx, 0, sizeof(*plugin_ctx));589590python_debug_deregister();591debug_return;592}593594PyObject *595python_plugin_api_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)596{597debug_decl(python_plugin_api_call, PYTHON_DEBUG_PY_CALLS);598599// Note: call fails if py_args is an empty tuple. Passing no arguments works passing NULL600// instead. So having such must be handled as valid. (See policy_plugin.validate())601if (py_args == NULL && PyErr_Occurred()) {602py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to build arguments for python plugin API call '%s'\n", func_name);603py_log_last_error(NULL);604debug_return_ptr(NULL);605}606607PyObject *py_callable = NULL;608py_callable = PyObject_GetAttrString(plugin_ctx->py_instance, func_name);609610if (py_callable == NULL) {611Py_CLEAR(py_args);612debug_return_ptr(NULL);613}614615py_debug_python_call(python_plugin_name(plugin_ctx), func_name,616py_args, NULL, PYTHON_DEBUG_PY_CALLS);617618PyObject *py_result = PyObject_CallObject(py_callable, py_args);619Py_CLEAR(py_args);620Py_CLEAR(py_callable);621622py_debug_python_result(python_plugin_name(plugin_ctx), func_name,623py_result, PYTHON_DEBUG_PY_CALLS);624625python_plugin_handle_plugin_error_exception(&py_result, plugin_ctx);626627if (PyErr_Occurred()) {628py_log_last_error(NULL);629}630631debug_return_ptr(py_result);632}633634int635python_plugin_rc_to_int(PyObject *py_result)636{637debug_decl(python_plugin_rc_to_int, PYTHON_DEBUG_PY_CALLS);638if (py_result == NULL)639debug_return_int(SUDO_RC_ERROR);640641if (py_result == Py_None)642debug_return_int(SUDO_RC_OK);643644debug_return_int((int)PyLong_AsLong(py_result));645}646647int648python_plugin_api_rc_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)649{650debug_decl(python_plugin_api_rc_call, PYTHON_DEBUG_PY_CALLS);651652PyObject *py_result = python_plugin_api_call(plugin_ctx, func_name, py_args);653int rc = python_plugin_rc_to_int(py_result);654Py_XDECREF(py_result);655debug_return_int(rc);656}657658int659python_plugin_show_version(struct PluginContext *plugin_ctx, const char *python_callback_name,660int is_verbose, unsigned int plugin_api_version, const char *plugin_api_name)661{662debug_decl(python_plugin_show_version, PYTHON_DEBUG_CALLBACKS);663664if (is_verbose) {665py_sudo_log(SUDO_CONV_INFO_MSG, "Python %s plugin (API %d.%d): %s (loaded from '%s')\n",666plugin_api_name,667SUDO_API_VERSION_GET_MAJOR(plugin_api_version),668SUDO_API_VERSION_GET_MINOR(plugin_api_version),669python_plugin_name(plugin_ctx),670plugin_ctx->plugin_path);671}672673int rc = SUDO_RC_OK;674if (PyObject_HasAttrString(plugin_ctx->py_instance, python_callback_name)) {675rc = python_plugin_api_rc_call(plugin_ctx, python_callback_name,676Py_BuildValue("(i)", is_verbose));677}678679debug_return_int(rc);680}681682void683python_plugin_close(struct PluginContext *plugin_ctx, const char *callback_name,684PyObject *py_args)685{686debug_decl(python_plugin_close, PYTHON_DEBUG_CALLBACKS);687688PyThreadState_Swap(plugin_ctx->py_interpreter);689690// Note, this should handle the case when init has failed691if (plugin_ctx->py_instance != NULL) {692if (!plugin_ctx->call_close) {693sudo_debug_printf(SUDO_DEBUG_INFO, "Skipping close call, because there was no command run\n");694695} else if (!PyObject_HasAttrString(plugin_ctx->py_instance, callback_name)) {696sudo_debug_printf(SUDO_DEBUG_INFO, "Python plugin function 'close' is skipped (not present)\n");697} else {698PyObject *py_result = python_plugin_api_call(plugin_ctx, callback_name, py_args);699py_args = NULL; // api call already freed it700Py_XDECREF(py_result);701}702}703704Py_CLEAR(py_args);705706if (PyErr_Occurred()) {707py_log_last_error(NULL);708}709710python_plugin_deinit(plugin_ctx);711PyThreadState_Swap(py_ctx.py_main_interpreter);712713debug_return;714}715716void717python_plugin_mark_callback_optional(struct PluginContext *plugin_ctx,718const char *function_name, void **function)719{720if (!PyObject_HasAttrString(plugin_ctx->py_instance, function_name)) {721debug_decl_vars(python_plugin_mark_callback_optional, PYTHON_DEBUG_PY_CALLS);722sudo_debug_printf(SUDO_DEBUG_INFO, "%s function '%s' is not implemented\n",723Py_TYPENAME(plugin_ctx->py_instance), function_name);724*function = NULL;725}726}727728const char *729python_plugin_name(struct PluginContext *plugin_ctx)730{731debug_decl(python_plugin_name, PYTHON_DEBUG_INTERNAL);732733const char *name = "(NULL)";734735if (plugin_ctx == NULL || !PyType_Check(plugin_ctx->py_class))736debug_return_const_str(name);737738debug_return_const_str(((PyTypeObject *)(plugin_ctx->py_class))->tp_name);739}740741void python_plugin_unlink(void) __attribute__((destructor));742743// this gets run only when sudo unlinks the python_plugin.so744void745python_plugin_unlink(void)746{747debug_decl(python_plugin_unlink, PYTHON_DEBUG_INTERNAL);748if (py_ctx.py_main_interpreter == NULL)749return;750751if (Py_IsInitialized()) {752sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit python %zu subinterpreters\n",753py_ctx.interpreter_count);754while (py_ctx.interpreter_count != 0) {755PyThreadState *py_interpreter =756py_ctx.py_subinterpreters[--py_ctx.interpreter_count];757PyThreadState_Swap(py_interpreter);758Py_EndInterpreter(py_interpreter);759}760761sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit main interpreter\n");762763// we need to call finalize from the main interpreter764PyThreadState_Swap(py_ctx.py_main_interpreter);765766if (Py_FinalizeEx() != 0) {767sudo_debug_printf(SUDO_DEBUG_WARN, "Closing: failed to deinit python interpreter\n");768}769770// Restore inittab so "sudo" module does not remain there (as garbage)771_restore_inittab();772}773py_ctx_reset();774debug_return;775}776777778