mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
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 <greg@krypto.org>
This commit is contained in:
parent
ce753517a8
commit
2666a06d33
9 changed files with 179 additions and 36 deletions
|
@ -23,25 +23,30 @@ information on handling these events.
|
||||||
The following events are raised internally and do not correspond to any
|
The following events are raised internally and do not correspond to any
|
||||||
public API of CPython:
|
public API of CPython:
|
||||||
|
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| Audit event | Arguments |
|
| Audit event | Arguments |
|
||||||
+==========================+===========================================+
|
+============================+===========================================+
|
||||||
| _winapi.CreateFile | ``file_name``, ``desired_access``, |
|
| _winapi.CreateFile | ``file_name``, ``desired_access``, |
|
||||||
| | ``share_mode``, ``creation_disposition``, |
|
| | ``share_mode``, ``creation_disposition``, |
|
||||||
| | ``flags_and_attributes`` |
|
| | ``flags_and_attributes`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.CreateJunction | ``src_path``, ``dst_path`` |
|
| _winapi.CreateJunction | ``src_path``, ``dst_path`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.CreateNamedPipe | ``name``, ``open_mode``, ``pipe_mode`` |
|
| _winapi.CreateNamedPipe | ``name``, ``open_mode``, ``pipe_mode`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.CreatePipe | |
|
| _winapi.CreatePipe | |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.CreateProcess | ``application_name``, ``command_line``, |
|
| _winapi.CreateProcess | ``application_name``, ``command_line``, |
|
||||||
| | ``current_directory`` |
|
| | ``current_directory`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.OpenProcess | ``process_id``, ``desired_access`` |
|
| _winapi.OpenProcess | ``process_id``, ``desired_access`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| _winapi.TerminateProcess | ``handle``, ``exit_code`` |
|
| _winapi.TerminateProcess | ``handle``, ``exit_code`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
| ctypes.PyObj_FromPtr | ``obj`` |
|
| _posixsubprocess.fork_exec | ``exec_list``, ``args``, ``env`` |
|
||||||
+--------------------------+-------------------------------------------+
|
+----------------------------+-------------------------------------------+
|
||||||
|
| ctypes.PyObj_FromPtr | ``obj`` |
|
||||||
|
+----------------------------+-------------------------------------------+
|
||||||
|
|
||||||
|
.. versionadded:: next
|
||||||
|
The ``_posixsubprocess.fork_exec`` internal audit event.
|
||||||
|
|
|
@ -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
|
.. 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.call_function`` with arguments ``function pointer`` and ``arguments``.
|
||||||
|
|
||||||
.. _ctypes-function-prototypes:
|
.. _ctypes-function-prototypes:
|
||||||
|
|
|
@ -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
|
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.
|
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
|
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.
|
Load a readline history file, and append it to the history list.
|
||||||
The default filename is :file:`~/.history`. This calls
|
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])
|
.. function:: write_history_file([filename])
|
||||||
|
|
||||||
Save the history list to a readline history file, overwriting any
|
Save the history list to a readline history file, overwriting any
|
||||||
existing file. The default filename is :file:`~/.history`. This calls
|
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])
|
.. 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
|
:file:`~/.history`. The file must already exist. This calls
|
||||||
:c:func:`!append_history` in the underlying library. This function
|
:c:func:`!append_history` in the underlying library. This function
|
||||||
only exists if Python was compiled for a version of the library
|
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
|
.. versionadded:: 3.5
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
The auditing event was added.
|
||||||
|
|
||||||
|
|
||||||
.. function:: get_history_length()
|
.. function:: get_history_length()
|
||||||
set_history_length(length)
|
set_history_length(length)
|
||||||
|
|
|
@ -195,6 +195,17 @@ def test_open(testfn):
|
||||||
except ImportError:
|
except ImportError:
|
||||||
load_dh_params = None
|
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.
|
# Try a range of "open" functions.
|
||||||
# All of them should fail
|
# All of them should fail
|
||||||
with TestHook(raise_on_events={"open"}) as hook:
|
with TestHook(raise_on_events={"open"}) as hook:
|
||||||
|
@ -204,6 +215,14 @@ def test_open(testfn):
|
||||||
(open, 3, "wb"),
|
(open, 3, "wb"),
|
||||||
(open, testfn, "w", -1, None, None, None, False, lambda *a: 1),
|
(open, testfn, "w", -1, None, None, None, False, lambda *a: 1),
|
||||||
(load_dh_params, testfn),
|
(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:
|
if not fn:
|
||||||
continue
|
continue
|
||||||
|
@ -229,6 +248,14 @@ def test_open(testfn):
|
||||||
(3, "w"),
|
(3, "w"),
|
||||||
(testfn, "w"),
|
(testfn, "w"),
|
||||||
(testfn, "rb") if load_dh_params else None,
|
(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
|
if i is not None
|
||||||
],
|
],
|
||||||
|
@ -278,6 +305,37 @@ def test_mmap():
|
||||||
assertEqual(hook.seen[0][1][:2], (-1, 8))
|
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 test_excepthook():
|
||||||
def excepthook(exc_type, exc_value, exc_tb):
|
def excepthook(exc_type, exc_value, exc_tb):
|
||||||
if exc_type is not RuntimeError:
|
if exc_type is not RuntimeError:
|
||||||
|
|
|
@ -80,6 +80,14 @@ class AuditTest(unittest.TestCase):
|
||||||
def test_mmap(self):
|
def test_mmap(self):
|
||||||
self.do_test("test_mmap")
|
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):
|
def test_excepthook(self):
|
||||||
returncode, events, stderr = self.run_python("test_excepthook")
|
returncode, events, stderr = self.run_python("test_excepthook")
|
||||||
if not returncode:
|
if not returncode:
|
||||||
|
|
|
@ -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.
|
|
@ -1198,6 +1198,12 @@ PyObject *_ctypes_callproc(ctypes_state *st,
|
||||||
void **avalues;
|
void **avalues;
|
||||||
PyObject *retval = NULL;
|
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);
|
n = argcount = PyTuple_GET_SIZE(argtuple);
|
||||||
#ifdef MS_WIN32
|
#ifdef MS_WIN32
|
||||||
/* an optional COM object this pointer */
|
/* an optional COM object this pointer */
|
||||||
|
@ -1673,10 +1679,6 @@ call_function(PyObject *self, PyObject *args)
|
||||||
&_parse_voidp, &func,
|
&_parse_voidp, &func,
|
||||||
&PyTuple_Type, &arguments))
|
&PyTuple_Type, &arguments))
|
||||||
return NULL;
|
return NULL;
|
||||||
if (PySys_Audit("ctypes.call_function", "nO",
|
|
||||||
(Py_ssize_t)func, arguments) < 0) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctypes_state *st = get_module_state(self);
|
ctypes_state *st = get_module_state(self);
|
||||||
result = _ctypes_callproc(st,
|
result = _ctypes_callproc(st,
|
||||||
|
@ -1710,10 +1712,6 @@ call_cdeclfunction(PyObject *self, PyObject *args)
|
||||||
&_parse_voidp, &func,
|
&_parse_voidp, &func,
|
||||||
&PyTuple_Type, &arguments))
|
&PyTuple_Type, &arguments))
|
||||||
return NULL;
|
return NULL;
|
||||||
if (PySys_Audit("ctypes.call_function", "nO",
|
|
||||||
(Py_ssize_t)func, arguments) < 0) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctypes_state *st = get_module_state(self);
|
ctypes_state *st = get_module_state(self);
|
||||||
result = _ctypes_callproc(st,
|
result = _ctypes_callproc(st,
|
||||||
|
|
|
@ -1203,6 +1203,19 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
|
||||||
goto cleanup;
|
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
|
/* 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
|
* want to call PyOS_BeforeFork() if there is any chance of another
|
||||||
* error leading to the cleanup: code without calling fork(). */
|
* error leading to the cleanup: code without calling fork(). */
|
||||||
|
|
|
@ -254,10 +254,21 @@ readline_read_init_file_impl(PyObject *module, PyObject *filename_obj)
|
||||||
if (filename_obj != Py_None) {
|
if (filename_obj != Py_None) {
|
||||||
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
if (PySys_Audit("open", "OCi", filename_obj, 'r', 0) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
errno = rl_read_init_file(PyBytes_AS_STRING(filename_bytes));
|
errno = rl_read_init_file(PyBytes_AS_STRING(filename_bytes));
|
||||||
Py_DECREF(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);
|
errno = rl_read_init_file(NULL);
|
||||||
|
}
|
||||||
if (errno)
|
if (errno)
|
||||||
return PyErr_SetFromErrno(PyExc_OSError);
|
return PyErr_SetFromErrno(PyExc_OSError);
|
||||||
disable_bracketed_paste();
|
disable_bracketed_paste();
|
||||||
|
@ -286,10 +297,19 @@ readline_read_history_file_impl(PyObject *module, PyObject *filename_obj)
|
||||||
if (filename_obj != Py_None) {
|
if (filename_obj != Py_None) {
|
||||||
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
if (PySys_Audit("open", "OCi", filename_obj, 'r', 0) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
errno = read_history(PyBytes_AS_STRING(filename_bytes));
|
errno = read_history(PyBytes_AS_STRING(filename_bytes));
|
||||||
Py_DECREF(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);
|
errno = read_history(NULL);
|
||||||
|
}
|
||||||
if (errno)
|
if (errno)
|
||||||
return PyErr_SetFromErrno(PyExc_OSError);
|
return PyErr_SetFromErrno(PyExc_OSError);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
@ -322,9 +342,17 @@ readline_write_history_file_impl(PyObject *module, PyObject *filename_obj)
|
||||||
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
||||||
return NULL;
|
return NULL;
|
||||||
filename = PyBytes_AS_STRING(filename_bytes);
|
filename = PyBytes_AS_STRING(filename_bytes);
|
||||||
|
if (PySys_Audit("open", "OCi", filename_obj, 'w', 0) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
filename_bytes = NULL;
|
filename_bytes = NULL;
|
||||||
filename = 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);
|
errno = err = write_history(filename);
|
||||||
int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length);
|
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))
|
if (!PyUnicode_FSConverter(filename_obj, &filename_bytes))
|
||||||
return NULL;
|
return NULL;
|
||||||
filename = PyBytes_AS_STRING(filename_bytes);
|
filename = PyBytes_AS_STRING(filename_bytes);
|
||||||
|
if (PySys_Audit("open", "OCi", filename_obj, 'a', 0) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
filename_bytes = NULL;
|
filename_bytes = NULL;
|
||||||
filename = 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(
|
errno = err = append_history(
|
||||||
nelements - libedit_append_replace_history_offset, filename);
|
nelements - libedit_append_replace_history_offset, filename);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue