https://github.com/python/cpython/commit/2666a06d336675247e1602aeb64170b2443602ce commit: 2666a06d336675247e1602aeb64170b2443602ce branch: main author: Robin Jadoul <robin.jad...@gmail.com> committer: gpshead <g...@krypto.org> date: 2025-04-13T21:46:20Z summary:
GH-115322: Add missing audit hooks (GH-115624) Add extra audit hooks to catch C function calling from ctypes, reading/writing files through readline and executing external programs through _posixsubprocess. * Make audit-tests for open pass when readline.append_history_file is unavailable * Less direct testing of _posixsubprocess for audit hooks * Also remove the audit hook from call_cdeclfunction now that _ctypes_callproc does it instead. * reword the NEWS entry. * mention readline in NEWS * add versionchanged markers * fix audit_events.rst versionadded * doc lint --------- Co-authored-by: Gregory P. Smith <g...@krypto.org> files: A Misc/NEWS.d/next/Security/2024-02-18-02-53-25.gh-issue-115322.Um2Sjx.rst M Doc/library/audit_events.rst M Doc/library/ctypes.rst M Doc/library/readline.rst M Lib/test/audit-tests.py M Lib/test/test_audit.py M Modules/_ctypes/callproc.c M Modules/_posixsubprocess.c M Modules/readline.c diff --git a/Doc/library/audit_events.rst b/Doc/library/audit_events.rst index a2a90a00d0cfca..d2377a38d78c81 100644 --- a/Doc/library/audit_events.rst +++ b/Doc/library/audit_events.rst @@ -23,25 +23,30 @@ information on handling these events. The following events are raised internally and do not correspond to any public API of CPython: -+--------------------------+-------------------------------------------+ -| Audit event | Arguments | -+==========================+===========================================+ -| _winapi.CreateFile | ``file_name``, ``desired_access``, | -| | ``share_mode``, ``creation_disposition``, | -| | ``flags_and_attributes`` | -+--------------------------+-------------------------------------------+ -| _winapi.CreateJunction | ``src_path``, ``dst_path`` | -+--------------------------+-------------------------------------------+ -| _winapi.CreateNamedPipe | ``name``, ``open_mode``, ``pipe_mode`` | -+--------------------------+-------------------------------------------+ -| _winapi.CreatePipe | | -+--------------------------+-------------------------------------------+ -| _winapi.CreateProcess | ``application_name``, ``command_line``, | -| | ``current_directory`` | -+--------------------------+-------------------------------------------+ -| _winapi.OpenProcess | ``process_id``, ``desired_access`` | -+--------------------------+-------------------------------------------+ -| _winapi.TerminateProcess | ``handle``, ``exit_code`` | -+--------------------------+-------------------------------------------+ -| ctypes.PyObj_FromPtr | ``obj`` | -+--------------------------+-------------------------------------------+ ++----------------------------+-------------------------------------------+ +| Audit event | Arguments | ++============================+===========================================+ +| _winapi.CreateFile | ``file_name``, ``desired_access``, | +| | ``share_mode``, ``creation_disposition``, | +| | ``flags_and_attributes`` | ++----------------------------+-------------------------------------------+ +| _winapi.CreateJunction | ``src_path``, ``dst_path`` | ++----------------------------+-------------------------------------------+ +| _winapi.CreateNamedPipe | ``name``, ``open_mode``, ``pipe_mode`` | ++----------------------------+-------------------------------------------+ +| _winapi.CreatePipe | | ++----------------------------+-------------------------------------------+ +| _winapi.CreateProcess | ``application_name``, ``command_line``, | +| | ``current_directory`` | ++----------------------------+-------------------------------------------+ +| _winapi.OpenProcess | ``process_id``, ``desired_access`` | ++----------------------------+-------------------------------------------+ +| _winapi.TerminateProcess | ``handle``, ``exit_code`` | ++----------------------------+-------------------------------------------+ +| _posixsubprocess.fork_exec | ``exec_list``, ``args``, ``env`` | ++----------------------------+-------------------------------------------+ +| ctypes.PyObj_FromPtr | ``obj`` | ++----------------------------+-------------------------------------------+ + +.. versionadded:: next + The ``_posixsubprocess.fork_exec`` internal audit event. diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index d3a81ce6bf7ea4..0bf88b0e3b66c6 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -1779,7 +1779,8 @@ in :mod:`!ctypes`) which inherits from the private :class:`_CFuncPtr` class: .. audit-event:: ctypes.call_function func_pointer,arguments foreign-functions - Some ways to invoke foreign function calls may raise an auditing event + Some ways to invoke foreign function calls as well as some of the + functions in this module may raise an auditing event ``ctypes.call_function`` with arguments ``function pointer`` and ``arguments``. .. _ctypes-function-prototypes: diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst index f297bdeec913a6..29e560cbc7a848 100644 --- a/Doc/library/readline.rst +++ b/Doc/library/readline.rst @@ -72,6 +72,12 @@ The following functions relate to the init file and user configuration: Execute a readline initialization file. The default filename is the last filename used. This calls :c:func:`!rl_read_init_file` in the underlying library. + It raises an :ref:`auditing event <auditing>` ``open`` with the file name + if given, and :code:`"<readline_init_file>"` otherwise, regardless of + which file the library resolves. + + .. versionchanged:: next + The auditing event was added. Line buffer @@ -109,14 +115,24 @@ The following functions operate on a history file: Load a readline history file, and append it to the history list. The default filename is :file:`~/.history`. This calls - :c:func:`!read_history` in the underlying library. + :c:func:`!read_history` in the underlying library + and raises an :ref:`auditing event <auditing>` ``open`` with the file + name if given and :code:`"~/.history"` otherwise. + + .. versionchanged:: next + The auditing event was added. .. function:: write_history_file([filename]) Save the history list to a readline history file, overwriting any existing file. The default filename is :file:`~/.history`. This calls - :c:func:`!write_history` in the underlying library. + :c:func:`!write_history` in the underlying library and raises an + :ref:`auditing event <auditing>` ``open`` with the file name if given and + :code:`"~/.history"` otherwise. + + .. versionchanged:: next + The auditing event was added. .. function:: append_history_file(nelements[, filename]) @@ -125,10 +141,14 @@ The following functions operate on a history file: :file:`~/.history`. The file must already exist. This calls :c:func:`!append_history` in the underlying library. This function only exists if Python was compiled for a version of the library - that supports it. + that supports it. It raises an :ref:`auditing event <auditing>` ``open`` + with the file name if given and :code:`"~/.history"` otherwise. .. versionadded:: 3.5 + .. versionchanged:: next + The auditing event was added. + .. function:: get_history_length() set_history_length(length) diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index af18ac2bb29c62..3d81f27e5cb46d 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -195,6 +195,17 @@ def test_open(testfn): except ImportError: load_dh_params = None + try: + import readline + except ImportError: + readline = None + + def rl(name): + if readline: + return getattr(readline, name, None) + else: + return None + # Try a range of "open" functions. # All of them should fail with TestHook(raise_on_events={"open"}) as hook: @@ -204,6 +215,14 @@ def test_open(testfn): (open, 3, "wb"), (open, testfn, "w", -1, None, None, None, False, lambda *a: 1), (load_dh_params, testfn), + (rl("read_history_file"), testfn), + (rl("read_history_file"), None), + (rl("write_history_file"), testfn), + (rl("write_history_file"), None), + (rl("append_history_file"), 0, testfn), + (rl("append_history_file"), 0, None), + (rl("read_init_file"), testfn), + (rl("read_init_file"), None), ]: if not fn: continue @@ -229,6 +248,14 @@ def test_open(testfn): (3, "w"), (testfn, "w"), (testfn, "rb") if load_dh_params else None, + (testfn, "r") if readline else None, + ("~/.history", "r") if readline else None, + (testfn, "w") if readline else None, + ("~/.history", "w") if readline else None, + (testfn, "a") if rl("append_history_file") else None, + ("~/.history", "a") if rl("append_history_file") else None, + (testfn, "r") if readline else None, + ("<readline_init_file>", "r") if readline else None, ] if i is not None ], @@ -278,6 +305,37 @@ def test_mmap(): assertEqual(hook.seen[0][1][:2], (-1, 8)) +def test_ctypes_call_function(): + import ctypes + import _ctypes + + with TestHook() as hook: + _ctypes.call_function(ctypes._memmove_addr, (0, 0, 0)) + assert ("ctypes.call_function", (ctypes._memmove_addr, (0, 0, 0))) in hook.seen + + ctypes.CFUNCTYPE(ctypes.c_voidp)(ctypes._memset_addr)(1, 0, 0) + assert ("ctypes.call_function", (ctypes._memset_addr, (1, 0, 0))) in hook.seen + + with TestHook() as hook: + ctypes.cast(ctypes.c_voidp(0), ctypes.POINTER(ctypes.c_char)) + assert "ctypes.call_function" in hook.seen_events + + with TestHook() as hook: + ctypes.string_at(id("ctypes.string_at") + 40) + assert "ctypes.call_function" in hook.seen_events + assert "ctypes.string_at" in hook.seen_events + + +def test_posixsubprocess(): + import multiprocessing.util + + exe = b"xxx" + args = [b"yyy", b"zzz"] + with TestHook() as hook: + multiprocessing.util.spawnv_passfds(exe, args, ()) + assert ("_posixsubprocess.fork_exec", ([exe], args, None)) in hook.seen + + def test_excepthook(): def excepthook(exc_type, exc_value, exc_tb): if exc_type is not RuntimeError: diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index b49938cb5008f3..2b24b5d79275fa 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -80,6 +80,14 @@ def test_cantrace(self): def test_mmap(self): self.do_test("test_mmap") + def test_ctypes_call_function(self): + import_helper.import_module("ctypes") + self.do_test("test_ctypes_call_function") + + def test_posixsubprocess(self): + import_helper.import_module("_posixsubprocess") + self.do_test("test_posixsubprocess") + def test_excepthook(self): returncode, events, stderr = self.run_python("test_excepthook") if not returncode: diff --git a/Misc/NEWS.d/next/Security/2024-02-18-02-53-25.gh-issue-115322.Um2Sjx.rst b/Misc/NEWS.d/next/Security/2024-02-18-02-53-25.gh-issue-115322.Um2Sjx.rst new file mode 100644 index 00000000000000..a09e1f1fcdcab7 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2024-02-18-02-53-25.gh-issue-115322.Um2Sjx.rst @@ -0,0 +1,4 @@ +The underlying extension modules behind :mod:`readline`:, :mod:`subprocess`, +and :mod:`ctypes` now raise audit events on previously uncovered code paths +that could lead to file system access related to C function calling and +external binary execution. diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c index 4e69c4e0a99ca2..f5db49ff4bc61c 100644 --- a/Modules/_ctypes/callproc.c +++ b/Modules/_ctypes/callproc.c @@ -1198,6 +1198,12 @@ PyObject *_ctypes_callproc(ctypes_state *st, void **avalues; PyObject *retval = NULL; + // Both call_function and call_cdeclfunction call us: + if (PySys_Audit("ctypes.call_function", "nO", + (Py_ssize_t)pProc, argtuple) < 0) { + return NULL; + } + n = argcount = PyTuple_GET_SIZE(argtuple); #ifdef MS_WIN32 /* an optional COM object this pointer */ @@ -1673,10 +1679,6 @@ call_function(PyObject *self, PyObject *args) &_parse_voidp, &func, &PyTuple_Type, &arguments)) return NULL; - if (PySys_Audit("ctypes.call_function", "nO", - (Py_ssize_t)func, arguments) < 0) { - return NULL; - } ctypes_state *st = get_module_state(self); result = _ctypes_callproc(st, @@ -1710,10 +1712,6 @@ call_cdeclfunction(PyObject *self, PyObject *args) &_parse_voidp, &func, &PyTuple_Type, &arguments)) return NULL; - if (PySys_Audit("ctypes.call_function", "nO", - (Py_ssize_t)func, arguments) < 0) { - return NULL; - } ctypes_state *st = get_module_state(self); result = _ctypes_callproc(st, diff --git a/Modules/_posixsubprocess.c b/Modules/_posixsubprocess.c index 1085e36cfe09b2..b542f86b6fe8da 100644 --- a/Modules/_posixsubprocess.c +++ b/Modules/_posixsubprocess.c @@ -1203,6 +1203,19 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args, goto cleanup; } + /* NOTE: user-defined classes may be able to present different + * executable/argument/env lists to the eventual exec as to this hook. + * The audit hook receives the original object, so would nevertheless + * be able to detect weird behaviour, hence we do not add extra + * complexity or performance penalties to attempt to avoid this. */ + if (PySys_Audit("_posixsubprocess.fork_exec", + "OOO", + executable_list, + process_args, + env_list) < 0) { + goto cleanup; + } + /* This must be the last thing done before fork() because we do not * want to call PyOS_BeforeFork() if there is any chance of another * error leading to the cleanup: code without calling fork(). */ diff --git a/Modules/readline.c b/Modules/readline.c index dd75f256d92142..0dd99dc66c08e9 100644 --- a/Modules/readline.c +++ b/Modules/readline.c @@ -254,10 +254,21 @@ readline_read_init_file_impl(PyObject *module, PyObject *filename_obj) if (filename_obj != Py_None) { if (!PyUnicode_FSConverter(filename_obj, &filename_bytes)) return NULL; + if (PySys_Audit("open", "OCi", filename_obj, 'r', 0) < 0) { + return NULL; + } errno = rl_read_init_file(PyBytes_AS_STRING(filename_bytes)); Py_DECREF(filename_bytes); - } else + } else { + /* We have the choice to either try to exactly reproduce the + * logic to find the filename, ignore it, or provide a dummy value. + * In contract to the history file manipulations, there's no + * clear default to choose. */ + if (PySys_Audit("open", "sCi", "<readline_init_file>", 'r', 0) < 0) { + return NULL; + } errno = rl_read_init_file(NULL); + } if (errno) return PyErr_SetFromErrno(PyExc_OSError); disable_bracketed_paste(); @@ -286,10 +297,19 @@ readline_read_history_file_impl(PyObject *module, PyObject *filename_obj) if (filename_obj != Py_None) { if (!PyUnicode_FSConverter(filename_obj, &filename_bytes)) return NULL; + if (PySys_Audit("open", "OCi", filename_obj, 'r', 0) < 0) { + return NULL; + } errno = read_history(PyBytes_AS_STRING(filename_bytes)); Py_DECREF(filename_bytes); - } else + } else { + /* Use the documented default filename here, + * even though readline expands it different internally. */ + if (PySys_Audit("open", "sCi", "~/.history", 'r', 0) < 0) { + return NULL; + } errno = read_history(NULL); + } if (errno) return PyErr_SetFromErrno(PyExc_OSError); Py_RETURN_NONE; @@ -322,9 +342,17 @@ readline_write_history_file_impl(PyObject *module, PyObject *filename_obj) if (!PyUnicode_FSConverter(filename_obj, &filename_bytes)) return NULL; filename = PyBytes_AS_STRING(filename_bytes); + if (PySys_Audit("open", "OCi", filename_obj, 'w', 0) < 0) { + return NULL; + } } else { filename_bytes = NULL; filename = NULL; + /* Use the documented default filename here, + * even though readline expands it different internally. */ + if (PySys_Audit("open", "sCi", "~/.history", 'w', 0) < 0) { + return NULL; + } } errno = err = write_history(filename); int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length); @@ -371,9 +399,17 @@ readline_append_history_file_impl(PyObject *module, int nelements, if (!PyUnicode_FSConverter(filename_obj, &filename_bytes)) return NULL; filename = PyBytes_AS_STRING(filename_bytes); + if (PySys_Audit("open", "OCi", filename_obj, 'a', 0) < 0) { + return NULL; + } } else { filename_bytes = NULL; filename = NULL; + /* Use the documented default filename here, + * even though readline expands it different internally. */ + if (PySys_Audit("open", "sCi", "~/.history", 'a', 0) < 0) { + return NULL; + } } errno = err = append_history( nelements - libedit_append_replace_history_offset, filename); _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com