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.
.. 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:
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
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])
: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)
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:
(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
(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
],
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:
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:
--- /dev/null
+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.
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 */
&_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,
&_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,
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(). */
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();
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;
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);
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);