1 /*
2  * SPDX-License-Identifier: ISC
3  *
4  * Copyright (c) 2019-2020 Robert Manner <robert.manner@oneidentity.com>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 /*
20  * This is an open source non-commercial project. Dear PVS-Studio, please check it.
21  * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
22  */
23 
24 #include "python_plugin_common.h"
25 #include "sudo_python_module.h"
26 
27 #include "sudo_queue.h"
28 #include "sudo_conf.h"
29 
30 #include <limits.h>
31 #include <string.h>
32 
33 static struct _inittab * python_inittab_copy = NULL;
34 static size_t python_inittab_copy_len = 0;
35 
36 #ifndef PLUGIN_DIR
37 #define PLUGIN_DIR ""
38 #endif
39 
40 /* Py_FinalizeEx is new in version 3.6 */
41 #if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION < 6
42 # define Py_FinalizeEx()	(Py_Finalize(), 0)
43 #endif
44 
45 const char *
_lookup_value(char * const keyvalues[],const char * key)46 _lookup_value(char * const keyvalues[], const char *key)
47 {
48     debug_decl(_lookup_value, PYTHON_DEBUG_INTERNAL);
49     if (keyvalues == NULL)
50         debug_return_const_str(NULL);
51 
52     size_t keylen = strlen(key);
53     for (; *keyvalues != NULL; ++keyvalues) {
54         const char *keyvalue = *keyvalues;
55         if (strncmp(keyvalue, key, keylen) == 0 && keyvalue[keylen] == '=')
56             debug_return_const_str(keyvalue + keylen + 1);
57     }
58     debug_return_const_str(NULL);
59 }
60 
61 CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
62 static int
_append_python_path(const char * module_dir)63 _append_python_path(const char *module_dir)
64 {
65     debug_decl(_append_python_path, PYTHON_DEBUG_PLUGIN_LOAD);
66     int rc = -1;
67     PyObject *py_sys_path = PySys_GetObject("path");
68     if (py_sys_path == NULL) {
69         PyErr_Format(sudo_exc_SudoException, "Failed to get python 'path'");
70         debug_return_int(rc);
71     }
72 
73     sudo_debug_printf(SUDO_DEBUG_DIAG, "Extending python 'path' with '%s'\n", module_dir);
74 
75     PyObject *py_module_dir = PyUnicode_FromString(module_dir);
76     if (py_module_dir == NULL || PyList_Append(py_sys_path, py_module_dir) != 0) {
77         Py_XDECREF(py_module_dir);
78         debug_return_int(rc);
79     }
80     Py_XDECREF(py_module_dir);
81 
82     if (sudo_debug_needed(SUDO_DEBUG_INFO)) {
83         char *path = py_join_str_list(py_sys_path, ":");
84         sudo_debug_printf(SUDO_DEBUG_INFO, "Python path became: %s\n", path);
85         free(path);
86     }
87 
88     rc = 0;
89     debug_return_int(rc);
90 }
91 
92 static PyObject *
_import_module(const char * path)93 _import_module(const char *path)
94 {
95     PyObject *module;
96     debug_decl(_import_module, PYTHON_DEBUG_PLUGIN_LOAD);
97 
98     sudo_debug_printf(SUDO_DEBUG_DIAG, "importing module: %s\n", path);
99 
100     char path_copy[PATH_MAX];
101     if (strlcpy(path_copy, path, sizeof(path_copy)) >= sizeof(path_copy))
102         debug_return_ptr(NULL);
103 
104     char *module_dir = path_copy;
105     char *module_name = strrchr(path_copy, '/');
106     if (module_name == NULL) {
107         module_name = path_copy;
108         module_dir = "";
109     } else {
110         *module_name++ = '\0';
111     }
112 
113     size_t len = strlen(module_name);
114     if (len >= 3 && strcmp(".py", module_name + len - 3) == 0)
115         module_name[len - 3] = '\0';
116 
117     sudo_debug_printf(SUDO_DEBUG_INFO, "module_name: '%s', module_dir: '%s'\n", module_name, module_dir);
118 
119     if (_append_python_path(module_dir) < 0)
120         debug_return_ptr(NULL);
121 
122     module = PyImport_ImportModule(module_name);
123     if (module != NULL) {
124 	PyObject *py_loaded_path = PyObject_GetAttrString(module, "__file__");
125 	if (py_loaded_path != NULL) {
126 	    const char *loaded_path = PyUnicode_AsUTF8(py_loaded_path);
127 	    /* If path is a directory, loaded_path may be a file inside it. */
128 	    if (strncmp(loaded_path, path, strlen(path)) != 0) {
129 		PyErr_Format(PyExc_Exception,
130 		    "module name conflict, tried to load %s, got %s",
131 		    path, loaded_path);
132 		Py_CLEAR(module);
133 	    }
134 	    Py_DECREF(py_loaded_path);
135 	}
136     }
137     debug_return_ptr(module);
138 }
139 
140 static PyThreadState *
_python_plugin_new_interpreter(void)141 _python_plugin_new_interpreter(void)
142 {
143     debug_decl(_python_plugin_new_interpreter, PYTHON_DEBUG_INTERNAL);
144     if (py_ctx.interpreter_count >= INTERPRETER_MAX) {
145         PyErr_Format(PyExc_Exception, "Too many interpreters");
146         debug_return_ptr(NULL);
147     }
148 
149     PyThreadState *py_interpreter = Py_NewInterpreter();
150     if (py_interpreter != NULL) {
151         py_ctx.py_subinterpreters[py_ctx.interpreter_count] = py_interpreter;
152         ++py_ctx.interpreter_count;
153     }
154 
155     debug_return_ptr(py_interpreter);
156 }
157 
158 static int
_save_inittab(void)159 _save_inittab(void)
160 {
161     debug_decl(_save_inittab, PYTHON_DEBUG_INTERNAL);
162     free(python_inittab_copy);  // just to be sure (it is always NULL)
163 
164     for (python_inittab_copy_len = 0;
165          PyImport_Inittab[python_inittab_copy_len].name != NULL;
166          ++python_inittab_copy_len) {
167     }
168     ++python_inittab_copy_len;  // for the null mark
169 
170     python_inittab_copy = malloc(sizeof(struct _inittab) * python_inittab_copy_len);
171     if (python_inittab_copy == NULL) {
172         debug_return_int(SUDO_RC_ERROR);
173     }
174 
175     memcpy(python_inittab_copy, PyImport_Inittab, python_inittab_copy_len * sizeof(struct _inittab));
176     debug_return_int(SUDO_RC_OK);
177 }
178 
179 static void
_restore_inittab(void)180 _restore_inittab(void)
181 {
182     debug_decl(_restore_inittab, PYTHON_DEBUG_INTERNAL);
183 
184     if (python_inittab_copy != NULL)
185         memcpy(PyImport_Inittab, python_inittab_copy, python_inittab_copy_len * sizeof(struct _inittab));
186 
187     free(python_inittab_copy);
188     python_inittab_copy = NULL;
189     python_inittab_copy_len = 0;
190     debug_return;
191 }
192 
193 void
python_plugin_handle_plugin_error_exception(PyObject ** py_result,struct PluginContext * plugin_ctx)194 python_plugin_handle_plugin_error_exception(PyObject **py_result, struct PluginContext *plugin_ctx)
195 {
196     debug_decl(python_plugin_handle_plugin_error_exception, PYTHON_DEBUG_INTERNAL);
197 
198     free(plugin_ctx->callback_error);
199     plugin_ctx->callback_error = NULL;
200 
201     if (PyErr_Occurred()) {
202         int rc = SUDO_RC_ERROR;
203         if (PyErr_ExceptionMatches(sudo_exc_PluginReject)) {
204             rc = SUDO_RC_REJECT;
205         } else if (!PyErr_ExceptionMatches(sudo_exc_PluginError)) {
206             debug_return;
207         }
208 
209         if (py_result != NULL) {
210             Py_CLEAR(*py_result);
211             *py_result = PyLong_FromLong(rc);
212         }
213 
214         PyObject *py_type = NULL, *py_message = NULL, *py_traceback = NULL;
215         PyErr_Fetch(&py_type, &py_message, &py_traceback);
216 
217         char *message = py_message ? py_create_string_rep(py_message) : NULL;
218         sudo_debug_printf(SUDO_DEBUG_INFO, "received sudo.PluginError exception with message '%s'",
219                           message == NULL ? "(null)" : message);
220 
221         plugin_ctx->callback_error = message;
222 
223         Py_CLEAR(py_type);
224         Py_CLEAR(py_message);
225         Py_CLEAR(py_traceback);
226     }
227 
228     debug_return;
229 }
230 
231 int
python_plugin_construct_custom(struct PluginContext * plugin_ctx,PyObject * py_kwargs)232 python_plugin_construct_custom(struct PluginContext *plugin_ctx, PyObject *py_kwargs)
233 {
234     debug_decl(python_plugin_construct_custom, PYTHON_DEBUG_PLUGIN_LOAD);
235     int rc = SUDO_RC_ERROR;
236     PyObject *py_args = PyTuple_New(0);
237 
238     if (py_args == NULL)
239         goto cleanup;
240 
241     py_debug_python_call(python_plugin_name(plugin_ctx), "__init__",
242                          py_args, py_kwargs, PYTHON_DEBUG_PY_CALLS);
243 
244     plugin_ctx->py_instance = PyObject_Call(plugin_ctx->py_class, py_args, py_kwargs);
245     python_plugin_handle_plugin_error_exception(NULL, plugin_ctx);
246 
247     py_debug_python_result(python_plugin_name(plugin_ctx), "__init__",
248                            plugin_ctx->py_instance, PYTHON_DEBUG_PY_CALLS);
249 
250     if (plugin_ctx->py_instance)
251         rc = SUDO_RC_OK;
252 
253 cleanup:
254     if (PyErr_Occurred()) {
255         py_log_last_error("Failed to construct plugin instance");
256         Py_CLEAR(plugin_ctx->py_instance);
257         rc = SUDO_RC_ERROR;
258     }
259 
260     Py_XDECREF(py_args);
261     debug_return_int(rc);
262 }
263 
264 PyObject *
python_plugin_construct_args(unsigned int version,char * const settings[],char * const user_info[],char * const user_env[],char * const plugin_options[])265 python_plugin_construct_args(unsigned int version,
266                         char *const settings[], char *const user_info[],
267                         char *const user_env[], char *const plugin_options[])
268 {
269     PyObject *py_settings = NULL;
270     PyObject *py_user_info = NULL;
271     PyObject *py_user_env = NULL;
272     PyObject *py_plugin_options = NULL;
273     PyObject *py_version = NULL;
274     PyObject *py_kwargs = NULL;
275 
276     if ((py_settings = py_str_array_to_tuple(settings)) == NULL ||
277         (py_user_info = py_str_array_to_tuple(user_info)) == NULL ||
278         (py_user_env = py_str_array_to_tuple(user_env)) == NULL ||
279         (py_plugin_options = py_str_array_to_tuple(plugin_options)) == NULL ||
280         (py_version = py_create_version(version)) == NULL ||
281         (py_kwargs = PyDict_New()) == NULL ||
282         PyDict_SetItemString(py_kwargs, "version", py_version) != 0 ||
283         PyDict_SetItemString(py_kwargs, "settings", py_settings) != 0 ||
284         PyDict_SetItemString(py_kwargs, "user_env", py_user_env) != 0 ||
285         PyDict_SetItemString(py_kwargs, "user_info", py_user_info) != 0 ||
286         PyDict_SetItemString(py_kwargs, "plugin_options", py_plugin_options) != 0)
287     {
288         Py_CLEAR(py_kwargs);
289     }
290 
291     Py_CLEAR(py_settings);
292     Py_CLEAR(py_user_info);
293     Py_CLEAR(py_user_env);
294     Py_CLEAR(py_plugin_options);
295     Py_CLEAR(py_version);
296     return py_kwargs;
297 }
298 
299 int
python_plugin_construct(struct PluginContext * plugin_ctx,unsigned int version,char * const settings[],char * const user_info[],char * const user_env[],char * const plugin_options[])300 python_plugin_construct(struct PluginContext *plugin_ctx, unsigned int version,
301                         char *const settings[], char *const user_info[],
302                         char *const user_env[], char *const plugin_options[])
303 {
304     debug_decl(python_plugin_construct, PYTHON_DEBUG_PLUGIN_LOAD);
305 
306     int rc = SUDO_RC_ERROR;
307     PyObject *py_kwargs = python_plugin_construct_args(
308         version, settings, user_info, user_env, plugin_options);
309 
310     if (py_kwargs == NULL) {
311         py_log_last_error("Failed to construct plugin instance");
312         rc = SUDO_RC_ERROR;
313     } else {
314         rc = python_plugin_construct_custom(plugin_ctx, py_kwargs);
315     }
316 
317     Py_CLEAR(py_kwargs);
318 
319     debug_return_int(rc);
320 }
321 
322 int
python_plugin_register_logging(sudo_conv_t conversation,sudo_printf_t sudo_printf,char * const settings[])323 python_plugin_register_logging(sudo_conv_t conversation,
324                                sudo_printf_t sudo_printf,
325                                char * const settings[])
326 {
327     debug_decl(python_plugin_register_logging, PYTHON_DEBUG_INTERNAL);
328 
329     int rc = SUDO_RC_ERROR;
330     if (conversation != NULL)
331         py_ctx.sudo_conv = conversation;
332 
333     if (sudo_printf)
334         py_ctx.sudo_log = sudo_printf;
335 
336     struct sudo_conf_debug_file_list debug_files = TAILQ_HEAD_INITIALIZER(debug_files);
337     struct sudo_conf_debug_file_list *debug_files_ptr = &debug_files;
338 
339     const char *plugin_path = _lookup_value(settings, "plugin_path");
340     if (plugin_path == NULL)
341         plugin_path = "python_plugin.so";
342 
343     const char *debug_flags = _lookup_value(settings, "debug_flags");
344 
345     if (debug_flags == NULL) {  // the group plugin does not have this information, so try to look it up
346         debug_files_ptr = sudo_conf_debug_files(plugin_path);
347     } else {
348         if (!python_debug_parse_flags(&debug_files, debug_flags))
349             goto cleanup;
350     }
351 
352     if (debug_files_ptr != NULL) {
353         if (!python_debug_register(plugin_path, debug_files_ptr))
354             goto cleanup;
355     }
356 
357     rc = SUDO_RC_OK;
358 
359 cleanup:
360     debug_return_int(rc);
361 }
362 
363 CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
364 static int
_python_plugin_register_plugin_in_py_ctx(void)365 _python_plugin_register_plugin_in_py_ctx(void)
366 {
367     debug_decl(_python_plugin_register_plugin_in_py_ctx, PYTHON_DEBUG_PLUGIN_LOAD);
368 
369     if (!Py_IsInitialized()) {
370         // Disable environment variables effecting the python interpreter
371         // This is important since we are running code here as root, the
372         // user should not be able to alter what is running any how.
373         Py_IgnoreEnvironmentFlag = 1;
374         Py_IsolatedFlag = 1;
375         Py_NoUserSiteDirectory = 1;
376 
377         if (_save_inittab() != SUDO_RC_OK)
378             debug_return_int(SUDO_RC_ERROR);
379 
380         PyImport_AppendInittab("sudo", sudo_module_init);
381         Py_InitializeEx(0);
382         py_ctx.py_main_interpreter = PyThreadState_Get();
383 
384         // This ensures we import "sudo" module in the main interpreter,
385         // each subinterpreter will have a shallow copy.
386         // (This makes the C sudo module able to eg. import other modules.)
387         PyObject *py_sudo = NULL;
388         if ((py_sudo = PyImport_ImportModule("sudo")) == NULL) {
389             debug_return_int(SUDO_RC_ERROR);
390         }
391         Py_CLEAR(py_sudo);
392     } else {
393         PyThreadState_Swap(py_ctx.py_main_interpreter);
394     }
395 
396     debug_return_int(SUDO_RC_OK);
397 }
398 
399 int
_python_plugin_set_path(struct PluginContext * plugin_ctx,const char * path)400 _python_plugin_set_path(struct PluginContext *plugin_ctx, const char *path)
401 {
402     if (path == NULL) {
403         py_sudo_log(SUDO_CONV_ERROR_MSG, "No python module path is specified. "
404                                          "Use 'ModulePath' plugin config option in 'sudo.conf'\n");
405         return SUDO_RC_ERROR;
406     }
407 
408     if (*path == '/') { // absolute path
409         plugin_ctx->plugin_path = strdup(path);
410     } else {
411         if (asprintf(&plugin_ctx->plugin_path, PLUGIN_DIR "/python/%s", path) < 0)
412             plugin_ctx->plugin_path = NULL;
413     }
414 
415     if (plugin_ctx->plugin_path == NULL) {
416         py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to allocate memory");
417         return SUDO_RC_ERROR;
418     }
419 
420     return SUDO_RC_OK;
421 }
422 
423 /* Returns the list of sudo.Plugins in a module */
424 static PyObject *
_python_plugin_class_list(PyObject * py_module)425 _python_plugin_class_list(PyObject *py_module) {
426     PyObject *py_module_dict = PyModule_GetDict(py_module);  // Note: borrowed
427     PyObject *key, *value; // Note: borrowed
428     Py_ssize_t pos = 0;
429     PyObject *py_plugin_list = PyList_New(0);
430 
431     while (PyDict_Next(py_module_dict, &pos, &key, &value)) {
432         if (PyObject_IsSubclass(value, (PyObject *)sudo_type_Plugin) == 1) {
433             if (PyList_Append(py_plugin_list, key) != 0)
434                 goto cleanup;
435         } else {
436             PyErr_Clear();
437         }
438     }
439 
440 cleanup:
441     if (PyErr_Occurred()) {
442         Py_CLEAR(py_plugin_list);
443     }
444     return py_plugin_list;
445 }
446 
447 /* Gets a sudo.Plugin class from the specified module. The argument "plugin_class"
448  * can be NULL in which case it loads the one and only "sudo.Plugin" present
449  * in the module (if so), or displays helpful error message. */
450 static PyObject *
_python_plugin_get_class(const char * plugin_path,PyObject * py_module,const char * plugin_class)451 _python_plugin_get_class(const char *plugin_path, PyObject *py_module, const char *plugin_class)
452 {
453     debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD);
454     PyObject *py_plugin_list = NULL, *py_class = NULL;
455 
456     if (plugin_class == NULL) {
457         py_plugin_list = _python_plugin_class_list(py_module);
458         if (py_plugin_list == NULL) {
459             goto cleanup;
460         }
461 
462         if (PyList_Size(py_plugin_list) == 1) {
463             PyObject *py_plugin_name = PyList_GetItem(py_plugin_list, 0); // Note: borrowed
464             plugin_class = PyUnicode_AsUTF8(py_plugin_name);
465         }
466     }
467 
468     if (plugin_class == NULL) {
469         py_sudo_log(SUDO_CONV_ERROR_MSG, "No plugin class is specified for python module '%s'. "
470                     "Use 'ClassName' configuration option in 'sudo.conf'\n", plugin_path);
471         if (py_plugin_list != NULL) {
472             /* Sorting the plugin list makes regress test output consistent. */
473             PyObject *py_obj = PyObject_CallMethod(py_plugin_list, "sort", "");
474             Py_CLEAR(py_obj);
475             char *possible_plugins = py_join_str_list(py_plugin_list, ", ");
476             if (possible_plugins != NULL) {
477                 py_sudo_log(SUDO_CONV_ERROR_MSG, "Possible plugins: %s\n", possible_plugins);
478                 free(possible_plugins);
479             }
480         }
481         goto cleanup;
482     }
483 
484     sudo_debug_printf(SUDO_DEBUG_DEBUG, "Using plugin class '%s'", plugin_class);
485     py_class = PyObject_GetAttrString(py_module, plugin_class);
486     if (py_class == NULL) {
487         py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to find plugin class '%s'\n", plugin_class);
488         PyErr_Clear();
489         goto cleanup;
490     }
491 
492     if (!PyObject_IsSubclass(py_class, (PyObject *)sudo_type_Plugin)) {
493         py_sudo_log(SUDO_CONV_ERROR_MSG, "Plugin class '%s' does not inherit from 'sudo.Plugin'\n", plugin_class);
494         Py_CLEAR(py_class);
495         goto cleanup;
496     }
497 
498 cleanup:
499     Py_CLEAR(py_plugin_list);
500     debug_return_ptr(py_class);
501 }
502 
503 int
python_plugin_init(struct PluginContext * plugin_ctx,char * const plugin_options[],unsigned int version)504 python_plugin_init(struct PluginContext *plugin_ctx, char * const plugin_options[],
505                    unsigned int version)
506 {
507     debug_decl(python_plugin_init, PYTHON_DEBUG_PLUGIN_LOAD);
508 
509     int rc = SUDO_RC_ERROR;
510 
511     if (_python_plugin_register_plugin_in_py_ctx() != SUDO_RC_OK)
512         goto cleanup;
513 
514     plugin_ctx->sudo_api_version = version;
515 
516     plugin_ctx->py_interpreter = _python_plugin_new_interpreter();
517     if (plugin_ctx->py_interpreter == NULL) {
518         goto cleanup;
519     }
520     PyThreadState_Swap(plugin_ctx->py_interpreter);
521 
522     if (!sudo_conf_developer_mode() && sudo_module_register_importblocker() < 0) {
523         goto cleanup;
524     }
525 
526     if (sudo_module_set_default_loghandler() < 0)
527         goto cleanup;
528 
529     if (_python_plugin_set_path(plugin_ctx, _lookup_value(plugin_options, "ModulePath")) != SUDO_RC_OK) {
530         goto cleanup;
531     }
532 
533     sudo_debug_printf(SUDO_DEBUG_DEBUG, "Loading python module from path '%s'", plugin_ctx->plugin_path);
534     plugin_ctx->py_module = _import_module(plugin_ctx->plugin_path);
535     if (plugin_ctx->py_module == NULL) {
536         goto cleanup;
537     }
538 
539     plugin_ctx->py_class = _python_plugin_get_class(plugin_ctx->plugin_path, plugin_ctx->py_module,
540                                                     _lookup_value(plugin_options, "ClassName"));
541     if (plugin_ctx->py_class == NULL) {
542         goto cleanup;
543     }
544 
545     rc = SUDO_RC_OK;
546 
547 cleanup:
548     if (plugin_ctx->py_class == NULL) {
549         py_log_last_error("Failed during loading plugin class");
550         rc = SUDO_RC_ERROR;
551     }
552 
553     debug_return_int(rc);
554 }
555 
556 void
python_plugin_deinit(struct PluginContext * plugin_ctx)557 python_plugin_deinit(struct PluginContext *plugin_ctx)
558 {
559     debug_decl(python_plugin_deinit, PYTHON_DEBUG_PLUGIN_LOAD);
560     sudo_debug_printf(SUDO_DEBUG_DIAG, "Deinit was called for a python plugin\n");
561 
562     Py_CLEAR(plugin_ctx->py_instance);
563     Py_CLEAR(plugin_ctx->py_class);
564     Py_CLEAR(plugin_ctx->py_module);
565 
566     // Note: we are preserving the interpreters here until the unlink because
567     // of bugs like (strptime does not work after python interpreter reinit):
568     // https://bugs.python.org/issue27400
569     // These potentially effect a lot more python functions, simply because
570     // it is a rare tested scenario.
571 
572     free(plugin_ctx->callback_error);
573     free(plugin_ctx->plugin_path);
574     memset(plugin_ctx, 0, sizeof(*plugin_ctx));
575 
576     python_debug_deregister();
577     debug_return;
578 }
579 
580 PyObject *
python_plugin_api_call(struct PluginContext * plugin_ctx,const char * func_name,PyObject * py_args)581 python_plugin_api_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)
582 {
583     debug_decl(python_plugin_api_call, PYTHON_DEBUG_PY_CALLS);
584 
585     // Note: call fails if py_args is an empty tuple. Passing no arguments works passing NULL
586     // instead. So having such must be handled as valid. (See policy_plugin.validate())
587     if (py_args == NULL && PyErr_Occurred()) {
588         py_sudo_log(SUDO_CONV_ERROR_MSG, "Failed to build arguments for python plugin API call '%s'\n", func_name);
589         py_log_last_error(NULL);
590         debug_return_ptr(NULL);
591     }
592 
593     PyObject *py_callable = NULL;
594     py_callable = PyObject_GetAttrString(plugin_ctx->py_instance, func_name);
595 
596     if (py_callable == NULL) {
597         Py_CLEAR(py_args);
598         debug_return_ptr(NULL);
599     }
600 
601     py_debug_python_call(python_plugin_name(plugin_ctx), func_name,
602                          py_args, NULL, PYTHON_DEBUG_PY_CALLS);
603 
604     PyObject *py_result = PyObject_CallObject(py_callable, py_args);
605     Py_CLEAR(py_args);
606     Py_CLEAR(py_callable);
607 
608     py_debug_python_result(python_plugin_name(plugin_ctx), func_name,
609                            py_result, PYTHON_DEBUG_PY_CALLS);
610 
611     python_plugin_handle_plugin_error_exception(&py_result, plugin_ctx);
612 
613     if (PyErr_Occurred()) {
614         py_log_last_error(NULL);
615     }
616 
617     debug_return_ptr(py_result);
618 }
619 
620 int
python_plugin_rc_to_int(PyObject * py_result)621 python_plugin_rc_to_int(PyObject *py_result)
622 {
623     debug_decl(python_plugin_rc_to_int, PYTHON_DEBUG_PY_CALLS);
624     if (py_result == NULL)
625         debug_return_int(SUDO_RC_ERROR);
626 
627     if (py_result == Py_None)
628         debug_return_int(SUDO_RC_OK);
629 
630     debug_return_int((int)PyLong_AsLong(py_result));
631 }
632 
633 int
python_plugin_api_rc_call(struct PluginContext * plugin_ctx,const char * func_name,PyObject * py_args)634 python_plugin_api_rc_call(struct PluginContext *plugin_ctx, const char *func_name, PyObject *py_args)
635 {
636     debug_decl(python_plugin_api_rc_call, PYTHON_DEBUG_PY_CALLS);
637 
638     PyObject *py_result = python_plugin_api_call(plugin_ctx, func_name, py_args);
639     int rc = python_plugin_rc_to_int(py_result);
640     Py_XDECREF(py_result);
641     debug_return_int(rc);
642 }
643 
644 int
python_plugin_show_version(struct PluginContext * plugin_ctx,const char * python_callback_name,int is_verbose,unsigned int plugin_api_version,const char * plugin_api_name)645 python_plugin_show_version(struct PluginContext *plugin_ctx, const char *python_callback_name,
646                            int is_verbose, unsigned int plugin_api_version, const char *plugin_api_name)
647 {
648     debug_decl(python_plugin_show_version, PYTHON_DEBUG_CALLBACKS);
649 
650     if (is_verbose) {
651         py_sudo_log(SUDO_CONV_INFO_MSG, "Python %s plugin (API %d.%d): %s (loaded from '%s')\n",
652                     plugin_api_name,
653                     SUDO_API_VERSION_GET_MAJOR(plugin_api_version),
654                     SUDO_API_VERSION_GET_MINOR(plugin_api_version),
655                     python_plugin_name(plugin_ctx),
656                     plugin_ctx->plugin_path);
657     }
658 
659     int rc = SUDO_RC_OK;
660     if (PyObject_HasAttrString(plugin_ctx->py_instance, python_callback_name)) {
661         rc = python_plugin_api_rc_call(plugin_ctx, python_callback_name,
662                                        Py_BuildValue("(i)", is_verbose));
663     }
664 
665     debug_return_int(rc);
666 }
667 
668 void
python_plugin_close(struct PluginContext * plugin_ctx,const char * callback_name,PyObject * py_args)669 python_plugin_close(struct PluginContext *plugin_ctx, const char *callback_name,
670                     PyObject *py_args)
671 {
672     debug_decl(python_plugin_close, PYTHON_DEBUG_CALLBACKS);
673 
674     PyThreadState_Swap(plugin_ctx->py_interpreter);
675 
676     // Note, this should handle the case when init has failed
677     if (plugin_ctx->py_instance != NULL) {
678         if (!plugin_ctx->call_close) {
679             sudo_debug_printf(SUDO_DEBUG_INFO, "Skipping close call, because there was no command run\n");
680 
681         } else if (!PyObject_HasAttrString(plugin_ctx->py_instance, callback_name)) {
682             sudo_debug_printf(SUDO_DEBUG_INFO, "Python plugin function 'close' is skipped (not present)\n");
683         } else {
684             PyObject *py_result = python_plugin_api_call(plugin_ctx, callback_name, py_args);
685             py_args = NULL;  // api call already freed it
686             Py_XDECREF(py_result);
687         }
688     }
689 
690     Py_CLEAR(py_args);
691 
692     if (PyErr_Occurred()) {
693         py_log_last_error(NULL);
694     }
695 
696     python_plugin_deinit(plugin_ctx);
697 
698     debug_return;
699 }
700 
701 void
python_plugin_mark_callback_optional(struct PluginContext * plugin_ctx,const char * function_name,void ** function)702 python_plugin_mark_callback_optional(struct PluginContext *plugin_ctx,
703                                      const char *function_name, void **function)
704 {
705     if (!PyObject_HasAttrString(plugin_ctx->py_instance, function_name)) {
706         debug_decl_vars(python_plugin_mark_callback_optional, PYTHON_DEBUG_PY_CALLS);
707         sudo_debug_printf(SUDO_DEBUG_INFO, "%s function '%s' is not implemented\n",
708                           Py_TYPENAME(plugin_ctx->py_instance), function_name);
709         *function = NULL;
710     }
711 }
712 
713 const char *
python_plugin_name(struct PluginContext * plugin_ctx)714 python_plugin_name(struct PluginContext *plugin_ctx)
715 {
716     debug_decl(python_plugin_name, PYTHON_DEBUG_INTERNAL);
717 
718     const char *name = "(NULL)";
719 
720     if (plugin_ctx == NULL || !PyType_Check(plugin_ctx->py_class))
721         debug_return_const_str(name);
722 
723     debug_return_const_str(((PyTypeObject *)(plugin_ctx->py_class))->tp_name);
724 }
725 
726 void python_plugin_unlink(void) __attribute__((destructor));
727 
728 // this gets run only when sudo unlinks the python_plugin.so
729 void
python_plugin_unlink(void)730 python_plugin_unlink(void)
731 {
732     debug_decl(python_plugin_unlink, PYTHON_DEBUG_INTERNAL);
733     if (py_ctx.py_main_interpreter == NULL)
734         return;
735 
736     if (Py_IsInitialized()) {
737         sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit python %zu subinterpreters\n",
738                           py_ctx.interpreter_count);
739         for (size_t i = 0; i < py_ctx.interpreter_count; ++i) {
740             PyThreadState *py_interpreter = py_ctx.py_subinterpreters[i];
741             PyThreadState_Swap(py_interpreter);
742             Py_EndInterpreter(py_interpreter);
743         }
744 
745         sudo_debug_printf(SUDO_DEBUG_NOTICE, "Closing: deinit main interpreter\n");
746 
747         // we need to call finalize from the main interpreter
748         PyThreadState_Swap(py_ctx.py_main_interpreter);
749 
750         if (Py_FinalizeEx() != 0) {
751             sudo_debug_printf(SUDO_DEBUG_WARN, "Closing: failed to deinit python interpreter\n");
752         }
753 
754         // Restore inittab so "sudo" module does not remain there (as garbage)
755         _restore_inittab();
756     }
757     py_ctx_reset();
758     debug_return;
759 }
760