Skip to content

Missing audit hooks in several extension modules #115322

Open
@RobinJadoul

Description

@RobinJadoul

Bug report

Bug description:

Several extension modules don't fully emit the relevant audit events, leading to file read or process spawning without any traceability.
In particular:

  • Calling a _ctypes.CFuncPtr does not emit ctypes.call_function. When combined with some known addresses, this can result in arbitrary functions in libc or python getting called. Such addresses could come from id, ctypes.pythonapi._handle, passing a byref pointer to ctypes.cast, or probably still several other methods. Coincidentally, the ctypes.cast method would by audited by the same ctypes.call_function once it is present. The downside is that it may also result in multiple audit hooks for functions like ctypes.string_at that have their own specialized audit hook event too.
  • Related, and maybe debatable, but constructing a _ctypes.CFuncPtr might fall under the audit event ctypes.cdata, as it is in spirit (though not in implementation) similar to calling a .from_address. An option might be to introduce ctypes.cdata/function similar to ctypes.cdata/buffer for this.
  • The readline module can open and read a file through readline.read_history_file without having an open audit hook. Together with readline.get_history_item, this can lead to unaudited file reads. A similar situation exists for some other functions in this library.
  • The _posixsubprocess.fork_exec function, and its only user in the standard library, multiprocessing.util.spawnv_passfds perform a fork + exec without any audit hooks. One would expect either os.fork and os.exec or the functionally similar os.posix_spawn here.

I'm happy to make a quick PR for these and adjust any specific event types to be more consistent or more uniquely identifiable.

Quick example in code:

import sys, ctypes, multiprocessing.util, readline

collected = []
def collector(event, *_args):
    collected.append(event)
sys.addaudithook(collector)

def test(fn, *args, **kw):
    collected.clear()
    fn(*args, **kw)
    if collected:  # Just check it's nonempty
        print("Success")
    else:
        print("Fail")

test(ctypes.memmove, 0, 0, 0)
test(ctypes.CFUNCTYPE(ctypes.py_object), ctypes._memmove_addr)
test(readline.read_history_file, __file__)
test(multiprocessing.util.spawnv_passfds, b"/bin/id", [], [])

CPython versions tested on:

3.11, 3.12, 3.13, CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

Labels

extension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions