FastAudit, Python Audit Hooks, and Sandboxing

Interactive quiz + study wiki. Everything is contained in this single HTML file.

Quiz

Answer the questions, then submit. You’ll get a score, explanations, and a copyable performance report.

Study Wiki

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 ctypes to 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:

  • tuple instead of list
  • frozenset instead of set
  • 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:

  1. Enter: set something up.
  2. Run the block.
  3. 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.”