Big picture
Python audit hooks are a way to observe sensitive runtime events. A hook receives an event name and event-specific arguments, then can allow the operation to continue or raise an error.
Audit event
An event raised by Python or user code, such as open,
subprocess.Popen, or a custom event like fastaudit.call.
Audit hook
A callback installed with sys.addaudithook. It is called as
hook(event, args).
FastAudit
A policy layer on top of audit hooks. It classifies events, checks permissions,
and can use sys.monitoring to catch calls into native code.
Audit hooks: what they receive
A normal Python audit hook receives two things:
def hook(event, args):
...
event: a string naming the event.args: event-specific arguments.
The hook does not receive a stack frame as a formal argument. However, because audit hooks
run synchronously while the operation is happening, code can inspect the current stack using
tools such as inspect.currentframe() or sys._getframe().
Why classify events?
FastAudit separates sensitive events into categories because different kinds of operations need different policies.
Default-deny events
Some operations are dangerous by nature. These are good candidates for default denial.
- Creating subprocesses.
- Loading native dynamic libraries.
- Using mechanisms like
ctypesto call native code. - Changing sensitive internals.
The core question is: should this kind of operation be allowed at all?
Directory-modifying events
Other operations are not inherently bad, but need location-based checks. Writing a file is often normal. The important question is where the write happens.
- Writing to an allowed temp/output directory may be fine.
- Writing to source files, home directories, or sensitive locations may be forbidden.
The core question is: is this filesystem mutation inside an allowed location?
Third-party libraries and audit coverage
Audit hooks only run when an audit event is raised. The Python standard library and CPython runtime raise many useful events. Third-party Python libraries may still trigger audit events transitively if they call audited stdlib operations.
third-party Python library
→ calls open() or os.remove()
→ CPython/stdlib raises audit event
→ audit hook sees it
But native extensions can bypass those audited paths.
third-party native extension
→ calls Rust/C filesystem API directly
→ no Python open() call
→ possibly no audit event
Dynamic libraries and ctypes
A dynamic library is compiled native code loaded at runtime.
- Linux:
.so - macOS:
.dylib - Windows:
.dll
ctypes is a Python standard-library module that allows Python code to load and call
functions from native dynamic libraries. This is powerful but sensitive, because native code can
bypass many Python-level controls.
sys.monitoring
sys.monitoring is a general low-level event system. It can report events such as
function calls. It does not specifically mean “native code call.” Instead, FastAudit can use
monitoring events to notice that a call happened, then classify the callable.
sys.monitoring: "a call happened"
FastAudit: "what kind of callable is this?"
FastAudit can then ignore ordinary Python calls, rely on existing stdlib audit events for
standard operations, and raise a custom event such as fastaudit.call for calls into
suspicious native/non-Python code.
The DISABLE performance trick
Monitoring every call can be expensive. Python’s monitoring API can allow a callback to disable future events for a particular call site. FastAudit can use this to say, in effect:
"This call site is safe; don't report it again."
Closures and immutable data
A closure is a function together with variables from the scope where it was created.
def outer(data):
def inner():
return data
return inner
Here, inner “closes over” data. It can still use data after
outer returns.
In a sandboxing/policy system, closures are useful because trusted policy data can live inside the audit machinery instead of in an obvious global variable that sandboxed code might mutate.
But a closure alone is not enough. The data should also be immutable:
tupleinstead oflistfrozensetinstead ofset- frozen/immutable dict-like structures instead of mutable dicts
Short version:
- Closure: hides or controls access to the reference.
- Immutable data: prevents mutation even if a reference is obtained.
Context managers
A context manager controls setup and cleanup around a block of code.
with something():
run_code_here()
It means:
- Enter: set something up.
- Run the block.
- Exit: clean up, even if an exception happened.
For FastAudit, a context manager can temporarily activate auditing policy while sandboxed code runs, then restore the previous state afterward.
Context variables
A ContextVar is like context-local state. It is safer than a plain global variable
when async tasks, nested runs, or concurrent execution are involved.
In FastAudit-style code, a context variable can hold the active audit config:
cfg = ctx.get()
if cfg is None:
return
chk(cfg, event, args)
This means:
- If no config is active in this execution context, ignore audit events.
- If a config is active, enforce that config’s policy.
Why tokens matter
tok = ctx.set(cfg) stores a new value and returns a token representing the previous
value. Later, ctx.reset(tok) restores that previous value.
This matters for nesting:
enter outer: ctx = cfg_outer
enter inner: ctx = cfg_inner
exit inner: ctx = cfg_outer
exit outer: ctx = previous value, often None
Without this, an inner sandbox could accidentally turn off or corrupt an outer sandbox.
ContextVar as stack-walking fallback
Sometimes code tries to find the relevant globals by walking stack frames. That can fail in async
cases such as asyncio.gather, because the logical execution context may no longer have
the original frame in the visible Python stack.
A ContextVar can store the relevant globals for the current execution context. Then,
if stack walking fails, the code can still recover the right environment.
Key mental models
- Audit hooks: “Tell me when sensitive runtime events happen.”
- FastAudit: “Apply policy to those events.”
- sys.monitoring: “Tell me when low-level execution events, like calls, happen.”
- Native code risk: “This may bypass Python-level audit events.”
- Closure: “Keep trusted policy data inside controlled machinery.”
- Immutable data: “Prevent the policy data from being rewritten.”
- Context manager: “Activate policy for this block, then restore.”
- ContextVar: “Store active config for this execution context, not the whole process.”