Skip to main content

Hisstrace

Programmable syscall interception and fault injection in Python.

strace, with fangs.

Pure stdlib, no dependencies. Pure Python ptrace bindings, 64-bit x86 and ARM.

Install

pip install hisstrace

What works

  • ptrace(2) via ctypes: no C extensions, no dependencies;
  • syscall number/name tables for x86-64 and AArch64, generated from kernel headers;
  • per-architecture register access (x86-64 uses GETREGS, AArch64 uses GETREGSET);
  • tracee memory reads and writes, plus Tracer.exe_image() / Tracer.exe_vaddr() for byte-signature scans of the executable, stripped binaries included;
  • a Tracer that spawns a command or attaches to a pid and iterates syscall stops as entry/exit events;
  • thread tracing: every thread is followed automatically, including threads created later (PTRACE_O_TRACECLONE);
  • child processes are opt-in: await on.fork() surfaces each new process, and child.follow() decides to trace it (the default is to let it run free);
  • attach to a running process and all its threads by pid (-p), following threads spawned after the attach;
  • software breakpoints inside the tracee: Tracer.set_breakpoint(addr) arms an int3, the stop surfaces as a Trap event with live registers, and the original instruction is single-stepped and re-armed for you;
  • argument mutation and fault injection at entry: on a pre-match, call.overwrite(count=1) rewrites named argument registers (names come from the syscall table), call.fail("ENOSPC") makes the call return -1 with that errno, and call.succeed(retval) skips the call and returns a fake success; the kernel never runs failed or succeeded calls, and the matching post-match sees the faked result;
  • hisstrace run CMD [ARGS...]: a strace-style printer, built from the scenario API like everything else.

Events carry raw register values: six unsigned arguments at entry, the raw return register at exit.

Scenarios

A scenario is one coroutine driven by syscall events:

from hisstrace import post, pre

async def scenario():
    done = await post.read(count=100)  # completion of the next matching read
    print(f"read: {done.data!r} (retval {done.retval})")

    call = await pre.write()  # next write stops before it enters the kernel
    print(f"write: {call.data!r}")

Run it against a command:

hisstrace scenario examples/capture.py -- python3 -c "..."

If all you want is strace, hisstrace run CMD is the familiar first look: every syscall, named arguments, results, errno names. It is built from the same scenario API. Reach for a scenario when you know what you are looking for: watch one fd, one path, one connection; rewrite arguments with overwrite; fail or fake results with fail/succeed; plant a breakpoint inside the process. examples/strace.py is the run printer written out as a scenario, ready to bend.

examples/httpforever.py prints curl's entire HTTP conversation with httpforever.com, request bytes marked -> and response bytes <-, by finding the TCP socket on port 80 and watching sendto/recvfrom on it. examples/byte_by_byte.py takes the idea one step further: it rewrites the size argument of every socket read and write to 1 (call.overwrite for count-style calls, one poke inside the msghdr for recvmsg/sendmsg), so the kernel moves the whole conversation one byte per syscall, and it tracks happy-eyeballs connection churn, forked transfers, and fd reuse to do it. examples/socket_audit.py is a linter for the "no timeout set" class of bug (the one where a library does a blocking call and one slow peer stalls the event loop): it tracks every socket the app creates (socket/accept4/socketpair), the fcntl(F_SETFL, O_NONBLOCK) and ioctl(FIONBIO) calls that make one non-blocking (what socket.setblocking(False) and os.set_blocking emit), treats a connect that returns EINPROGRESS as proof of non-blocking, and warns at close time about any socket that lived its whole life blocking, naming the peer it was connected to. examples/nologin_audit.py is the same shape pointed at trust rather than performance: run /usr/sbin/nologin (or /bin/false) and it watches every syscall entry against a whitelist before the kernel runs it, lists every opened path, and calls out terminal access, execs, unexpected connects, or off-whitelist syscalls, with a verdict at the end. examples/watch_conn.py traces a full SSH round of a Go client (socket traffic across its thread handoff plus every getrandom, ending when the connection closes); examples/ssh_decrypted.py goes the last mile: it plants a breakpoint on crypto/ecdh.x25519ScalarMult inside the process, recovers the X25519 shared secret from the trap's registers, derives the session keys, and decrypts the whole session in pure Python. examples/ssh_rng_via_syscall.py does the same decryption with no symbols and no breakpoints at all: a one-byte patch forces Go's getrandom off the vDSO and onto the syscall path, the KEX scalar is picked out of the syscall stream, and the session decrypts from there (this one example needs pip install cryptography; hisstrace itself has no dependencies).

Or attach to a running process (every thread is traced, and threads spawned later are picked up automatically):

hisstrace run -p 4242
hisstrace scenario examples/trackfd.py -p 4242

Contract, v1: one scenario runs at a time. While Python code between awaits runs, the matched tracee stays stopped; reaching the next await resumes it. Unmatched syscalls pass through unchanged. post.X correlates entry and exit of one invocation and returns a CompletedCall (entry args plus retval, error, succeeded, failed, and a capped data snapshot for read/write). any_of(...) awaits several matchers and resumes with whichever fires first:

while True:
    event = await any_of(post.read(fd=fd), pre.write(fd=fd), pre.close(fd=fd))
    if event.name == "close":
        break
    print(event.name, event.data)

See examples/trackfd.py for the full fd-tracking loop. pre.any() and post.any() watch every syscall in that phase, which is all hisstrace run is: a strace built from the scenario API, with argument names for every syscall (generated from kernel prototypes) and C-string reads for pointer arguments. examples/strace.py is that printer written out as a scenario, exit_group's strace-style = ? line included, ready to bend to your own formatting:

while True:
    done = await post.any()
    print(done.name, dict(done.args), done.retval)
    # done.string("path") reads a NUL-terminated string at that argument

Child processes are the exception to pass-through: a new process stays frozen until the scenario decides. await on.fork() delivers it; child.follow() traces it, otherwise it runs untraced. Threads need no decision, they are always followed.

child = await on.fork()
child.follow()          # or don't: the child runs free
print("new process", child.pid, "from thread", child.parent_tid)

When the coroutine returns, a spawned tracee keeps running untraced and run() waits it out and returns its exit status; an attached tracee is detached and run() returns 0. data is None for syscalls without a buffer; reads that fail give b"". Match filters compare against raw kernel argument values, so remember that Python adds O_CLOEXEC to os.open before the kernel sees the flags.

Named arguments come from a table generated out of the kernel's syscall prototypes (461 syscalls; make table regenerates), with arg0..arg5 as the fallback for anything missing, and are also usable as match filters, as count=100 above shows.

Honest limits

One scenario runs at a time, serially. At entry a scenario can rewrite arguments (overwrite) and the outcome (fail, succeed), but it cannot swap one syscall for another, and an in-flight call cannot be cancelled, only steered. No fd-lifetime tracking: dup2 aliasing and fd reuse after close are invisible to plain fd= filters (scenarios track fds themselves, as examples/byte_by_byte.py does). Attaching to another user's process fails with the kernel's ptrace_scope error.

Develop

make test    # unittest against src/
make lint    # ruff + mypy + line counts
make build   # wheel into dist/
make table   # regenerate syscall tables from kernel headers

Metadata

Release files for hisstrace 2026.10.29

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hisstrace 2026.10.29
File Size Uploaded Provenance
hisstrace-2026.10.29.tar.gz 38.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hisstrace 2026.10.29
File Interpreter ABI Platform
hisstrace-2026.10.29-py3-none-any.whl Python 3 none any Details

Total release size: 69.3 kB

Release files / hisstrace-2026.10.29.tar.gz

Download URL hisstrace-2026.10.29.tar.gz
Size 38.3 kB
Tags Source
SHA-256 checksum
How to use checksums
032400ca5639d6583cd241a6f2d9380aa68e5a66355b5d7ffe7b7870943e796b
BLAKE2b-256 checksum
How to use checksums
cd671d84221c1a25b034bf1b5326b9264d74b2b2a7db7a3f6c0c6f399ef93817
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Release files / hisstrace-2026.10.29-py3-none-any.whl

Download URL hisstrace-2026.10.29-py3-none-any.whl
Size 30.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
24553b2baeeb3f0434c2499a0c8f548ace1d3ec1df4846861e7e3a578af08bf3
BLAKE2b-256 checksum
How to use checksums
3d4eb02041ef32f42dc715d67bb5d2f58aab10d536db8ec2181c643d320e7c40
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Security for hisstrace 2026.10.29

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page