Userspace libc interceptor · v2.2.2
see
Observe every libc call. Arguments, return values, durations — in JSON or interactive HTML.
control
Rewrite arguments and return values on the way through. No source. No recompile.
break
Fault the call: OOM, short I/O, latency, deny-list. Find the bugs your tests can't reach.
13
platforms
12
actions
~200 KB
binary
0
recompiles
~/proj — retrace trace
Why retrace exists

Every existing tool does part of the job. None does all of it.

strace observes. Frida mutates. libFuzzer fuzzes. Each is excellent at its verb and silent on the other two. retrace is the first tool that does all three — observe, mutate, break — on any dynamically-linked binary, without source, without recompiling, on thirteen platforms.

strace

Tells you what happened.

Cannot modify arguments, override returns, or inject failures. Observe-only.

Frida

Lets you script it.

Requires JavaScript. 40 MB runtime. Heavyweight for simple interception.

libFuzzer

Finds bugs in your own code.

Requires source and recompilation. Cannot target binaries you didn't build.

retrace
see+control+break=every libc call, your decision

One ~200 KB shared library. Twelve built-in actions. JSON config you can write in five minutes. Runs on the binary you already have.

Trust

The receipts. Read them before you install.

retrace preloads a shared library into the target process. That's the same mechanism that malware uses to inject code. You should be skeptical. Here's what we have to say about it.

License
BSD-2-Clause
No copyleft. Use in commercial, academic, and proprietary code.
Telemetry
None
No network calls, no phone-home, no usage data. The library doesn't even depend on a network stack.
Dependencies
Zero at runtime
Links only against the platform's libc. No third-party C libraries. No JavaScript runtime. No Python.
Code of conduct
Ribose community
Same standard Ribose applies to its open-source projects: respectful, technical, no jerks.
Maintainer
Ribose
Open-source subsidiary of Ribose, an ISO/IEC 27001 + 27701 certified company. Active since 2017.
CVE status
None reported
No known security vulnerabilities in retrace itself. Tracked via GitHub Security Advisories.
One command

Detects your OS and architecture. Downloads the right binary. No compiler, no Python, no Docker required.

Works on Linux (glibc + musl + OHOS), macOS, FreeBSD/OpenBSD/NetBSD, Windows, Android.

$curl -sSL https://raw.githubusercontent.com/riboseinc/retrace/main/scripts/install.sh | sh
01 · INSTALL
The script puts retrace on your PATH and libretrace in your loader path. About five seconds.
# detect OS + arch, fetch the right binary$ curl -sSL .../install.sh | sh
retrace 2.2.2 installed
02 · TRACE
Trace any binary. Add --html for an interactive report. No source. No recompile. No config file.
$ retrace trace malloc,free \
      --html -- /bin/ls
wrote /tmp/retrace-43892.html
03 · OPEN
Self-contained HTML — summary cards, category breakdown, filterable call table. No Python, no server, no CDN.
$ open /tmp/retrace-43892.html

  1,247 calls · 48.3 ms · 42 functions
  [ I/O ]  485 calls  32.1 ms (66%)
  [ MEM ]  412 calls   8.3 ms (17%)
Try it without installing

Click a command. See what retrace would do.

Every output below mirrors what the real retrace binary produces. Use this to learn the command syntax and the output shape before you install. When you're ready to run it on your own binary, the install takes about five seconds.

Try a command:

Simulated. Output mirrors what the real retrace binary produces — try the install to run it on your own binary.

~/playground — retrace
How it works

One shared library sits between your binary and libc.

retrace preloads a small (~200 KB) shared library into the target process. Every libc call routes through a single assembly trampoline per function. You decide — per call, per script — what happens at each of the three taps.

Caller
your binary
——→
retrace wrapper
trampoline + engine
——→
Real libc
malloc, open, …
▸ TAP 1 — SEE
Inspect arguments before the call. log_params emits args as JSON.call_real lets it proceed.
▸ TAP 2 — CONTROL
Rewrite the call. modify_in_param_str, modify_in_param_int,modify_return_value_int.
▸ TAP 3 — BREAK
Make it fail. memory_fuzz, incomplete_io, delay,call_count_limit, sandbox.
Performance

How much does this actually cost?

The honest answer: single-digit microseconds per call, plus JSON serialization proportional to argument size. Most programs see 5–15% wall-clock overhead under default logging. You can dial that down to near-zero if you only want the side effects (mock, sandbox, fault injection) and don't need the log.

Per-call overhead
Single-digit µs

Assembly trampoline → C engine → JSON serialization. No interpreter in the hot loop. Most of the cost is the log line itself, not the interception.

Wall-clock impact
5–15% typical

Under default logging (log_params + call_real for every function). Drops sharply when you restrict to a function allowlist.

Memory
~10 MB + log

Engine + parson + per-thread state is small. Log file grows linearly with intercepted calls; rotate or pipe to /dev/null for long runs.

Library size
~200 KB

Statically linked assembly trampolines + engine + parson. No third-party C libraries. No JavaScript. No Python.

Tuning

Four levers, in order of impact.

  1. 01
    Restrict the function allowlist
    RETRACE_LOGGER_ALLOWED_FUNCS

    Comma-separated list of function names to log. The engine still intercepts every function (so call_real, modify_*, etc. still work), but the JSON log only emits lines for the allowlisted ones. Biggest single win.

  2. 02
    Suppress stdout mirroring
    RETRACE_LOGGER_DEF_STDOUT_ENA=0

    Pair with --log FILE. stdout mirroring has its own serialization cost; if you only need the file, disable mirroring.

  3. 03
    Skip log_params for modify-only scripts
    (config)

    If you're using retrace to mutate (modify_in_param_*, modify_return_value_int) but don't care about observation, omit log_params from the action list. The engine still runs but emits no log line for that call.

  4. 04
    Disable logging entirely
    RETRACE_LOGGER_DEF_ENA=0

    Interception still runs (so sandbox, mock, fuzz all still work), but no log output anywhere. Use when you only want the side effects.

Shines

  • Debugging — why is this slow, what's it reading
  • Testing — fault injection in CI
  • Audit — what does this binary actually do
  • Reverse engineering — map closed-source behavior
  • Security — sandbox, deny-list, secret-sniffing

Don't

  • Production hot paths without RETRACE_LOGGER_ALLOWED_FUNCS
  • Real-time / latency-sensitive inner loops
  • Kernelspace (use eBPF instead)
  • Anything requiring strict bit-exact reproducibility (timings alter under trace)
Twelve built-in actions

Composable primitives, declared in JSON.

Each function in your config carries an ordered list of actions. Add a new behavior by dropping one.c file into src/core/actions/ — no engine change, no recompile of the binary you're tracing.

Observe
Read-only inspection
log_paramsargs + return
call_realinvoke libc
fuzzing_seeddeterministic RNG
Modify
Rewrite the call
modify_in_param_strrewrite string
modify_in_param_intrewrite integer
modify_in_param_arrrewrite bytes
modify_return_value_intoverride return
Fault
Inject failure
memory_fuzzrandom OOM
incomplete_ioshort read/write
Control
Shape the run
delaylatency inject
call_count_limitexhaust resource
sandboxpath deny-list
Function catalog

What can you intercept? About eighty common calls.

retrace ships with prototypes for nearly three hundred libc functions out of the box. Below is a curated tour of the most-traced, grouped by what they're for. Click any function to see its signature and the one-liner to trace it. Use retrace list-functionsfor the full list.

79 of 79 functions
File I/O
16

The hot path for most programs. Trace these to see what a binary actually reads or writes.

  • openint open(const char *path, int flags, ...)
  • openatint openat(int dirfd, const char *path, int flags, ...)
  • closeint close(int fd)
  • readssize_t read(int fd, void *buf, size_t count)
  • preadssize_t pread(int fd, void *buf, size_t count, off_t offset)
  • writessize_t write(int fd, const void *buf, size_t count)
  • fopenFILE *fopen(const char *path, const char *mode)
  • fcloseint fclose(FILE *stream)
  • freadsize_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • fwritesize_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  • fgetschar *fgets(char *s, int size, FILE *stream)
  • fputsint fputs(const char *s, FILE *stream)
  • dupint dup(int oldfd)
  • dup2int dup2(int oldfd, int newfd)
  • pipeint pipe(int pipefd[2])
  • popenFILE *popen(const char *command, const char *type)
Memory
9

Allocators and bulk byte operations. The first stop for memory-leak and OOM investigations.

  • mallocvoid *malloc(size_t size)
  • callocvoid *calloc(size_t nmemb, size_t size)
  • reallocvoid *realloc(void *ptr, size_t size)
  • freevoid free(void *ptr)
  • brkint brk(void *addr)
  • memcpyvoid *memcpy(void *dest, const void *src, size_t n)
  • memsetvoid *memset(void *s, int c, size_t n)
  • memmovevoid *memmove(void *dest, const void *src, size_t n)
  • memcmpint memcmp(const void *s1, const void *s2, size_t n)
Process & exec
12

Fork, exec, system. Audit these for command injection, sandbox escape, and unexpected subprocesses.

  • forkpid_t fork(void)
  • execveint execve(const char *path, char *const argv[], char *const envp[])
  • execvpint execvp(const char *file, char *const argv[])
  • execlint execl(const char *path, const char *arg, ...)
  • execlpint execlp(const char *file, const char *arg, ...)
  • systemint system(const char *command)
  • popenFILE *popen(const char *command, const char *type)
  • exitvoid exit(int status)
  • _exitvoid _exit(int status)
  • abortvoid abort(void)
  • chrootint chroot(const char *path)
  • niceint nice(int inc)
Time
5

Read the clock or sleep. Mock these to freeze time, replay schedules, or test expiry.

  • timetime_t time(time_t *t)
  • ctime_rchar *ctime_r(const time_t *timep, char *buf)
  • localtime_rstruct tm *localtime_r(const time_t *timep, struct tm *result)
  • alarmunsigned int alarm(unsigned int seconds)
  • pauseint pause(void)
Identity
8

Who am I? Mock these to make a binary think it's root, in a different login session, etc.

  • getuiduid_t getuid(void)
  • geteuiduid_t geteuid(void)
  • getgidgid_t getgid(void)
  • getegidgid_t getegid(void)
  • getgroupsint getgroups(int size, gid_t list[])
  • getloginchar *getlogin(void)
  • getpidpid_t getpid(void)
  • getppidpid_t getppid(void)
File system
8

Paths the binary touches. Pair with the sandbox action to deny sensitive locations.

  • chdirint chdir(const char *path)
  • getcwdchar *getcwd(char *buf, size_t size)
  • chownint chown(const char *path, uid_t owner, gid_t group)
  • fchownint fchown(int fd, uid_t owner, gid_t group)
  • lchownint lchown(const char *path, uid_t owner, gid_t group)
  • linkint link(const char *oldpath, const char *newpath)
  • ftruncateint ftruncate(int fd, off_t length)
  • accessint access(const char *path, int mode)
Pthreads
8

Locks, condition variables, rwlocks. Profile these to find contention and deadlocks.

  • pthread_createint pthread_create(pthread_t *thread, ...)
  • pthread_joinint pthread_join(pthread_t thread, void **retval)
  • pthread_mutex_lockint pthread_mutex_lock(pthread_mutex_t *mutex)
  • pthread_mutex_unlockint pthread_mutex_unlock(pthread_mutex_t *mutex)
  • pthread_cond_waitint pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
  • pthread_cond_signalint pthread_cond_signal(pthread_cond_t *cond)
  • pthread_rwlock_rdlockint pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
  • pthread_rwlock_wrlockint pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
Environment
1

Env var reads. Audit these to find a binary sniffing for secrets, debug flags, or backdoors.

  • getenvchar *getenv(const char *name)
Strings & ctype
8

Character classification and bulk byte ops. Often surprisingly hot.

  • isalphaint isalpha(int c)
  • isdigitint isdigit(int c)
  • isspaceint isspace(int c)
  • isprintint isprint(int c)
  • tolowerint tolower(int c)
  • toupperint toupper(int c)
  • mblenint mblen(const char *s, size_t n)
  • mbtowcint mbtowc(wchar_t *pwc, const char *s, size_t n)
Dynamic loading
4

Plugins, drivers, runtime-loaded libraries. Audit these to find supply-chain attack surface.

  • dlopenvoid *dlopen(const char *filename, int flags)
  • dlcloseint dlclose(void *handle)
  • dlerrorchar *dlerror(void)
  • dlsymvoid *dlsym(void *handle, const char *symbol)
Who uses retrace

Twenty-five jobs. One shared library.

retrace is small enough to slip into a one-off debugging session, generic enough to anchor a CI fuzzing pipeline, principled enough to back a security audit. Below: every audience that reaches for it, and the command they reach for first.

security
see

Security researcher

Reverse-engineer a binary you don't have source for. Map every open, every connect, every system call — in seconds, without a disassembler.

$ retrace trace open,connect,system -- ./suspicious
qa
break

QA engineer

Find error-path bugs without writing fixtures. Inject OOM, short I/O, and latency directly into your test runs in CI.

$ retrace fuzz malloc --rate 0.1 -- ./server
dev
control

Backend developer

Find the hot path. See per-call timings on every libc call. Redirect a file open to a stub without rebuilding.

$ retrace trace --html -- ./api-server
devops
see

DevOps / SRE

Trace any containerized binary in one line. Drop the library into your image; no recompile, no restart, no APM agent.

$ RUN curl -sSL .../libretrace-linux.so -o /usr/lib/libretrace.so
pentest
break

Penetration tester

Sandbox an untrusted binary at runtime. Block /etc/shadow, /root/.ssh/, network egress — without SELinux or AppArmor.

$ retrace run --config sandbox.json -- ./untrusted
edu
control

Educator / student

See exactly what ls, cat, grep actually do. The trace is the lesson — every libc call, in order, with timings.

$ retrace trace --html -- /bin/ls
ctf
control

CTF player

Solve tracing challenges fast. Watch the binary leak the flag through write, find the comparison via strcmp, time the side channel.

$ retrace trace strcmp,write -- ./challenge
re
see

Reverse engineer

Understand a closed-source binary's behavior without firing up IDA. Trace the calls; reconstruct the logic from observable I/O.

$ retrace trace -- ./closed-source-binary
forensics
see

Forensics analyst

Post-incident analysis: replay a captured trace to see exactly what a process did — files touched, sockets opened, env vars read.

$ retrace pp /tmp/trace.json
bounty
break

Bug bounty hunter

Find vulns by exhausting resources. Make the 1000th malloc fail, the 50th open return ENOENT, see if the program survives.

$ retrace run --config limit-then-fail.json -- ./target
mobile
control

Mobile developer

Trace any Android app's native libraries via wrap.sh or Magisk. Cross-compile once with the NDK; same JSON config as your desktop.

$ LD_PRELOAD=libretrace.so /data/local/tmp/wrap.sh
sysadmin
see

System administrator

Diagnose why a service is misbehaving without restarting it. Trace a single command's libc activity to see what's really being read, written, connected to.

$ retrace trace --html -- $(which dig) example.com
crypto
control

Cryptography auditor

Verify a binary uses the right crypto primitives. Trace RAND_bytes, EVP_DigestInit, SSL_CTX_set_verify — see every algorithm and parameter.

$ retrace trace RAND_bytes,EVP_* -- ./crypto-app
embedded
break

Embedded developer

Stress-test firmware error paths. Inject short reads on flash, ENOSPC on write, timeouts on i2c-sysfs — without desoldering a thing.

$ retrace run --config incomplete-io.json -- ./firmware-test
lib author
see

Library author

Confirm your library's ABI surface in practice. Trace which functions downstream callers actually invoke, with what arguments, and how often.

$ retrace trace 'mylib_*' -- ./downstream-app
red team
break

Red team operator

Test defensive controls end-to-end. Sandbox your own dropper, fuzz its malloc paths to find crash points defenders could exploit, verify EDR detection logic triggers.

$ retrace run --config sandbox-and-fuzz.json -- ./dropper
IR
see

Incident responder

Capture a malicious binary's libc activity in a controlled sandbox. Re-run the captured trace to build an evidence timeline without giving the binary network access.

$ retrace run --config sandbox.json --log /tmp/evidence.json -- ./seized-binary
perf
see

Performance engineer

Quantify libc overhead before optimizing. See per-function totals across the whole run, then zoom into the top 1% of calls by duration.

$ retrace pp /tmp/trace.json | head -10
vr
break

Vulnerability researcher

Hunt for zero-days by exhausting every boundary. Make the 1000th call fail, the buffer exactly full, the path exactly too long. See what breaks.

$ retrace run --config edge-cases.json -- ./target
game
control

Game developer

Trace native game libraries. Find the asset load that's slow, the audio callback that allocates, the render path that reads from disk mid-frame.

$ retrace trace fopen,read,mmap -- ./game-binary
compiler
see

Compiler engineer

Validate your libc intercept before shipping. Trace every call to a target function across all your test workloads; flag any unexpected arg shape.

$ retrace trace 'your_lib_*' --log ./baseline.json -- ./test-suite
build
control

Build / release engineer

Wire fault-injection into CI. Run the test suite under memory_fuzz on every PR; catch OOM-as-bug before users do.

$ retrace fuzz malloc --rate 0.05 -- ./run-tests
db
see

Database engineer

Trace a DB client's libc surface to find the slow query path. See which read/write syscalls each query triggers, and where time goes.

$ retrace trace read,write,fsync --log /tmp/db.json -- ./db-client
dist
control

Distributed systems engineer

Reproduce a flaky consensus vote by mocking time, fuzzing malloc, and forcing fsync to fail. The bug only happens under specific conditions — retrace makes them deterministic.

$ retrace run --config consensus-repro.json -- ./raft-node
ml
see

ML engineer

Debug a Python/C++ native binding. Trace the interpreter's libc calls to find which model-loading step is bottlenecked on disk I/O vs. computation.

$ retrace trace fopen,read,mmap -- python3 train.py
Don't know where to start?

Pick a path. We've charted it for you.

Twenty-two tutorials is a lot to navigate. Below are six pre-defined sequences through them, each chosen for a specific role or goal. Click a chip to see the sequence — every step links into the matching tutorial below.

🌱Beginner: first hour with retrace

Three tutorials that take you from zero to your first HTML trace report.

New to retrace? Start here.

  1. 01
    tutorial: source

    Build retrace from source so you have the latest. (Or skip to step 2 if you curl-installed.)

  2. 02
    tutorial: slow

    Run your first trace, generate the HTML report, see the category breakdown.

  3. 03
    tutorial: oom

    Now break something. Inject 10% OOM into malloc and watch your binary's error paths.

Not sure where to start?

Answer three questions and we'll generate the right config + command for your use case.

1. Goal2. Sub-goal3. Target4. Result

What do you want to do?

Tutorials

Pick your scenario. We'll walk you through it.

Every newcomer's question is the same: "I have this problem — how do I use retrace to solve it?"Twenty-two scenarios below cover the questions users actually ask. Click yours on the left; the steps appear on the right with copy-paste commands and expected output. No prior retrace knowledge assumed.

⌛ see

Find what's making my program slow

Trace every libc call with timings, then sort by total time.

DebuggingPerformance
  1. 01

    Install retrace if you haven't already.

    curl -sSL https://raw.githubusercontent.com/riboseinc/retrace/main/scripts/install.sh | sh
    retrace 2.2.2 installed
  2. 02

    Run your program under retrace with HTML output. The --html flag generates a self-contained interactive page.

    retrace trace --html --log /tmp/trace.json -- ./your-program
    wrote /tmp/retrace-43892.html
  3. 03

    Open the report.

    open /tmp/retrace-43892.html
    (browser opens with summary cards + filterable table)
  4. 04

    Look at the category breakdown. The biggest category by total time is your suspect. Click any function to filter the table.

    [ I/O ]   485 calls  32.1 ms (66%)
    [ MEM ]   412 calls   8.3 ms (17%)
    ...
  5. 05

    For a CLI-only view of per-function totals, pretty-print the JSON log:

    retrace pp /tmp/trace.json | head -10
    open          48 calls   12.4ms total
    read         124 calls    8.7ms total
    ...
Thirteen platforms, one library

From Linux on a Raspberry Pi to Windows on Arm.

Per-platform assembly trampolines behind a uniform engine. The JSON config you write on your Mac runs unchanged on your Android device, your FreeBSD CI runner, and your Windows Server.

OS
Arch
Status
Linux (glibc)
x86_64, aarch64
production
Linux (musl)
x86_64, aarch64
production
Linux (OHOS)
aarch64
production
macOS
arm64, x86_64
production
FreeBSD / OpenBSD / NetBSD
x86_64
production
Windows (MSVC)
x86_64, arm64
production
Windows (MinGW)
x86_64
production
Android
arm64, x86_64
production
Linux (static binary)
x86_64, aarch64
via ptrace

Same engine, different trampolines. ELF, Mach-O, and PE binaries each get a from-scratch assembly trampoline — no MinHook, no Detours, BSD-2 clean.

Binary releases for every platform are attached to eachGitHub release. The install script picks the right one.

Docker image atghcr.io/riboseinc/retrace:latest — drop into any container with a one-lineRUN.

How it compares

Observe and mutate, without writing code.

The existing tools each do part of the job. strace observes but cannot mutate. Frida mutates but requires JavaScript. libFuzzer mutates but requires recompilation. retrace is the only one that does all three on a binary you didn't build.

CapabilityretracestraceFridalibFuzzer
Observe libc calls
Modify arguments
Override return value
Fault injection (OOM, short I/O)built-inpartial
No source code needed
No recompilation
Cross-platform13 platformspartialpartial
Binary size~200 KBpreinstalled~40 MBlinked in
Declarative configJSON / CLI
Recipes

Twenty cookbook recipes. Four favorites to start with.

Each recipe is self-contained: copy the JSON, run the command, see the result. Browse thefull cookbookfor all twenty.

Recipe Builder

Visually compose a config: pick a function, drag actions from the palette, tune params inline, copy the generated JSON into the validator.

Function

Use * for wildcard. Common:

Action chain

  1. 1. log_params
  2. 2. call_real

Drag actions here to add, or click:

Generated config

{
  "intercept_scripts": [
    {
      "func_name": "malloc",
      "actions": [
        {
          "action_name": "log_params"
        },
        {
          "action_name": "call_real"
        }
      ]
    }
  ]
}

Paste into the Config Validator to verify, then save as /tmp/recipe.json.

Validate your config

Paste a JSON config. Get immediate feedback.

The browser equivalent of retrace validate. JSON parses in your page; nothing is sent anywhere. Five pre-baked examples below the input let you see what valid (and invalid) configs look like.

Your JSON config
Load an example:
Result
ok: 1 script(s), 2 action(s)
This config will run. Save it as config.json and launch with retrace run --config config.json -- ./your-binary.
Recently shipped · What's next

The supervisor arc landed: 24 releases, both OSes, one protocol.

retrace has shipped continuously since 2017. The list below is what landed in the latest release and what's queued for the next two. Starthe repo or watchreleases to follow along.

13
platforms
12
actions
317
functions
21
cookbook recipes
0
external deps
SHIPPED: v2.38 - v2.73

The arc that made retrace a supervised instrument: daemon, fleet control, enforcement, three observation lanes, and the Windows lane made real.

v2.78.0
control

Departures are journal records

The reap doctrine: a SIGCHLD self-pipe routes every spawned workload's death to the daemon's poll loop, which reaps and journals retrace.ctl.exit {pid, how, code}. A specimen that crashes, is killed, or leaves on its own is never a silent gap in the audit trail -- and never a zombie either.

v2.77.0
control

One list, every surface

The control plane's verbs were four hand-synced edits (dispatch chain, scope gate, CLI builder, usage text -- which had already drifted). Now one X-macro list is the SSOT: the daemon's dispatch table, every TLS claim scope, and the CLI's own usage derive from it. A verb is one line plus one handler. The extraction also caught a real defect: every events reply was truncating at 64 bytes.

v2.76.0
control

The launch arm: retrace-ctl spawn

One command forks a workload armed to join the daemon itself -- supervisor env, the nonce, an eager agent, the caller's preload. The launch is journaled before the child can act, and the child takes a full seat: never a spectator, because the nonce traveled with the fork. Launch, life, and kill now all ride the control plane.

v2.75.0
see

Session trees at any depth

The sessions walker goes recursive: worker trees and fork-bomb detonations chain past any fixed scan level, and depth stops lying at level 4. The tree the registry carries is the tree the CLI prints, at every depth.

v2.74.0
break

mmap and munmap, intercepted

The prototypes the very first upstream PR (#414) asked for, on today's rails: memory maps are now first-class intercepted calls -- arguments, returns, and the ability to fail or fuzz them like every other boundary crossing.

v2.73.0
control

The evidence plane: retrace-ctl events

The journal's tail over the control plane, with the hash-chain verdict riding the reply -- evidence pulled over a network carries its own integrity statement, and a tampered line names its own line number. STATUS-scoped by design: an auditor reads evidence without touching even the registry. The fleet CLI is now control AND evidence.

v2.72.0
see

Session trees, as trees

retrace-ctl sessions prints the tree the registry always carried -- session tokens, fork parents, spectator seats -- nested instead of flat. Detonation-farm operators stop re-nesting forks by eye across JSON lines.

v2.71.0
break

fail_first: the retry-path primitive

Fault injection could exhaust a resource but never recover from one. fail_first fails the first N calls with a chosen error (the real call never made), then lets every later call through -- a correct retry loop converges on call N+1, an untested one hangs where retrace can see it.

v2.70.0
see

noderetrace: the runtime trio completes

The Node runtime agent -- diagnostics_channel for the runtime's own boundary, UDS and named-pipe transports, the same supervise/emit surface as its Python and JVM siblings. Three independent implementations, three hook systems, one protocol: the conformance claim's strongest evidence.

v2.67.0
control

Packages, and artifacts that carry the tools

CPack now rides the install surface: every tarball and zip carries bin/ -- retraced, the fleet CLI, enforcement, all the converters (they were missing from releases entirely before) -- and the Linux legs ship dpkg-validated .deb packages. RPM is configured for source builds.

v2.63-66
see

The agent's last twins, unified

One frame codec (the Windows arm adopted the shared encoder and its payload cap, closing a silent oversized-event loss), one policy validation ladder in policy_sig.c (the Windows copy had lost the expiry guard), and one pure event ring with drop accounting on both halves.

v2.63.0
control

Policy crosses the pipe

The fleet CLI's policy push now reaches Windows pipe agents for the first time -- the broadcast sends through an installed sink over transport-opaque handles, and the codebase's last extern-as-interface retired.

v2.62.0
control

The Windows lane goes real

The named-pipe daemon, fleet CLI, and all four agents (libc, kernel-ETW, Python, JVM) now build, link, and pass their integration tests on Windows -- including the SCM service lane (sc create/start/stop with graceful journal flush) and a configure-time conformance guard over the everywhere-build set. Sixteen defects that had shipped dark were burned off in one activation arc.

v2.53-61
see

Runtime agents: Python and JVM

pyretrace and jretrace attribute file opens, sockets, and execs to the module and line that issued them -- the layer a libc interposer sees only as an open from pid N. Both are third-party implementations pinned to the same conformance suite as the reference stub.

v2.52-59
see

Kernel observation, graded live

eBPF and ETW bridge agents join every session as kernel-lane spectators; the daemon grades kernel observations against libc claims per sweep and streams drift summaries -- the correlate oracle's signal, live.

v2.50-58
control

Kernel enforcement, audited

One declared-set, four enforcement planes: Landlock, seccomp, Seatbelt, and AppContainer -- each exec bound to a hash-chained, Ed25519-signed audit trail that fails closed on tamper.

v2.54-56
break

Signed policy with rotation

POLICY_SET ships as an Ed25519-signed wrapper over the exact policy bytes; the daemon pins multiple trust keys so rotation overlaps instead of breaking the fleet.

v2.38-46
see

The supervisor daemon

retraced: per-host registry, hash-chained journal, formally-versioned control protocol, and the nonce doctrine -- nonceless HELLOs seat as spectators: evidence always, policy never.

v2.35-36
see

Live OTLP streaming

RETRACE_OTLP_ENDPOINT streams OpenTelemetry spans live from inside the traced process -- one env var, no batch pipeline. Spans, security events, and metrics land in Grafana/Jaeger/otelcol as calls happen.

v2.36.0
break

Security events over OTLP

Jail denials, fuzz crash clusters, and drift-oracle hits emit as OTLP log records with a documented schema -- policy violations watchable live during detonations.

v2.31.0
control

Static-CRT Windows binaries

/MT binaries carry their own CRT -- no ucrtbase to hook. Their file access is now observed and jailed through the ntdll layer, proven by a true static-CRT CI smoke.

v2.27.0
see

ETW kernel-truth capture

Raw ETW events captured on CI runners and converted with the OS's own provider manifests -- the numeric Id-to-task table is transcribed from Get-WinEvent, not docs.

v2.7-34
control

Kernel-truth converters

strace, dtrace, truss, ktrace, procmon, and ETW traces all convert to the retrace shape -- five platforms of kernel-layer ground truth for grading libc claims.

v2.29-33
break

Fuzzing workbench

Crash clustering, minimized corpora, per-cluster reproducers, a drift oracle (behavior the baseline never saw), and dictionary grammar templates -- one command, one report.

v2.10+
control

Live jail + hardening

retrace-profile jail emits a ready-to-run sandbox config; the sandbox action enforces it at runtime with deny/allow lists, decoy dirs, and env visibility.

v2.37.0
see

Deepening pass: inventory conformance

Every backend's function inventory is now conformance-tested against the shared list -- the class of 'silently missing 28 functions on one arch' bug is a red check, not a coverage hole.

ROADMAP

What's coming in v2.2.0 and v2.3.0.

  • 01

    Live eBPF legs on CI

    next

    The eBPF agent runs --synthetic in the matrix; the real loader needs a BPF-capable (self-hosted) runner leg so kernel-lane captures are exercised on every PR, not only locally.

  • 02

    Cookbook growth

    ongoing

    The recipe set covers the classic, security-research, and resilience flows (fail_first's retry-path recipe landed with v2.71); what's missing is user-contributed war stories.

  • 03

    Brew + rpm channels

    next

    The .deb ships from the releases (dpkg-validated in CI) and cpack builds rpm from source; the remaining channels are a Homebrew tap and an rpm repository so package managers see updates.

Get involved

Picked a path? Here's how to go further.

retrace is BSD-2-Clause open source. The three doors below cover the common reasons people come back after their first successful trace. Pick the one that matches your situation.

Frequently asked

The questions every evaluator asks.

Eight years of issues, PR reviews, and conference hallway-track conversations have distilled into these twelve. If yours isn't here, open anissue.

Does retrace work with my language?

Yes, as long as the program compiles to a native binary. C, C++, Rust, Go (with cgo enabled or on Linux where Go's runtime uses libc for some syscalls), Swift, Zig, D, Pascal — all work. Pure managed runtimes (JIT-compiled Java, JavaScript, pure-Go without cgo) make fewer libc calls and are less useful to trace, but the libc calls they do make are still captured. Interpreters (Python, Ruby, Node) — retrace traces the interpreter's own libc activity, which often reveals what the script is doing.

Does it work on stripped binaries?

Yes. retrace intercepts at the libc symbol level, not the binary's own symbols. A fully stripped binary still calls libc functions by name; those are the symbols retrace hooks. You don't need debug info, source, or symbols in the target.

Does it work on PIE / ASLR binaries?

Yes. Position-Independent Executables and ASLR don't affect retrace — interposition happens at the dynamic linker level, before the binary's load address matters. The same goes for stacked/relro/fortify hardening flags.

How much overhead does it add?

Single-digit microseconds per intercepted call, plus JSON serialization proportional to argument size. Most programs see wall-clock overhead of 5–15% under default logging. Crank RETRACE_LOGGER_ALLOWED_FUNCS down to the functions you care about and overhead drops sharply. The fork+engine path is assembly; there is no interpreter in the hot loop.

Can I use retrace in production?

Yes, with care. Logs may contain argument values (file paths, buffer contents, environment variable names) — scrub or redact before persisting. For long-running processes, log to a file with rotation, not stdout. The sandbox and mock actions are stateless and safe; the fuzzing actions are not — they will crash your program by design.

Is retrace safe to run on the binary I'm investigating?

retrace itself doesn't modify the target binary — it preloads a library and intercepts calls at runtime. The target runs with its normal privileges under your normal user. The untrusted-binary workflow (sandbox action, deny-list) is specifically designed for running code you don't trust; see cookbook recipe 20.

How does retrace compare to eBPF?

Different layers. eBPF runs in kernel context, observes syscalls, and can't (portably) modify arguments or skip calls. retrace runs in userspace, observes libc calls, and can mutate arguments, override return values, and inject failures. They're complementary — eBPF for system-wide observability, retrace for per-process control. eBPF is Linux-only; retrace works on 13 platforms.

How does it compare to strace?

strace observes syscalls (kernel boundary) and writes them as text. It cannot modify arguments, override returns, or inject failures. retrace observes libc calls (userspace boundary), can mutate them, runs on more platforms, and produces structured JSON plus interactive HTML. Use strace for 'what is this binary doing right now?' — use retrace for 'what does this binary do under failure conditions I control?'

Can I write my own actions?

Yes. Add one .c file under src/core/actions/ that registers itself via the RETRACE_ACTION_REGISTER macro. The action receives the thread context (parsed args, return value pointer) and the JSON action_params. No engine change needed. See src/core/actions/basic.c for the pattern.

What's the deal with macOS SIP?

Apple's System Integrity Protection blocks DYLD_INSERT_LIBRARIES for binaries in /usr/bin and similar protected paths. Either copy the target to /tmp/ first, or disable SIP via csrutil (not recommended for daily use). Third-party binaries in /Applications or your home directory are unaffected.

Is the JSON log format stable?

Within a major version, yes. New fields may be added (parse defensively); existing fields are not renamed or removed. See the configuration reference for the schema. The interactive HTML viewer handles whatever fields are present.

How do I cite retrace in academic work?

Cite the repository: riboseinc/retrace, version 2.2.2, BSD-2-Clause license, available at https://github.com/riboseinc/retrace. There is no formal paper yet; if you'd like one, open an issue.

Glossary

Every term the rest of the page uses, in one place.

retrace has its own vocabulary — trampolines, wrappers, frames, scripts, prototypes. The definitions below are the canonical reference. Cross-references point to related terms.

The core model
7 terms
Interception

What retrace does to a libc call. The call routes through retrace's wrapper before reaching libc, so retrace can observe, modify, or fail it.

Trampoline

A small piece of assembly that 'bounces' a libc call into retrace's engine. One per intercepted function. Lives in src/backends/preload_*/<arch>/arch_spec_top.S.

See also: Wrapper, Frame

Wrapper

The retrace-defined symbol that replaces a libc function in the target's symbol table. Caller invokes the wrapper; wrapper runs the engine; engine decides whether to invoke the real libc function.

See also: Engine, Real impl

Engine

The C function (retrace_engine_wrapper) that receives every wrapped call. Looks up the matching intercept script, parses args, runs the script's actions in order, and synthesizes the return value.

See also: Action, Script

Frame

A C struct (WrapperSystemVFrame or WrapperAArch64Frame) that captures the call's register arguments. The engine reads args from the frame and writes the return value back into it.

Real impl

The actual libc function (e.g., the real malloc). retrace resolves these once at init via dlsym(RTLD_NEXT, ...) and stores them in a function-pointer struct. call_real invokes the matching pointer.

See also: Real-impl indirection

Real-impl indirection

The pattern where every libc call inside retrace itself goes through the retrace_real_impls struct, never direct. This is the reentrancy guard — bypass it and you recurse.

Configuration
5 terms
Script (intercept script)

A JSON object binding a function name (or wildcard) to an ordered list of actions. The unit of configuration.

See also: Action

Action

A named primitive that runs when its script matches. log_params, call_real, modify_in_param_*, modify_return_value_int, memory_fuzz, incomplete_io, delay, call_count_limit, sandbox, fuzzing_seed. New actions = one .c file, no engine change.

See also: Script

Prototype

A C struct (struct FuncPrototype) describing a function's name, calling convention, return type, and parameter types. Lives in src/core/prototypes/. The engine uses prototypes to parse args from the frame.

JSON config

A file containing one or more intercept scripts. Loaded at startup via RETRACE_JSON_CONFIG. Default config (no file) is log_params + call_real for every function.

Log

The JSON-lines output stream. One object per intercepted call, with function name, arguments, return value, and call_duration_us. View with retrace pp (text) or retrace html (interactive).

Mechanism
5 terms
Preload

Setting LD_PRELOAD (ELF), DYLD_INSERT_LIBRARIES (Darwin), or in-process inline hooking (Windows) so the dynamic linker resolves libc symbols to retrace's wrappers instead of the real ones.

Backend

A platform-specific implementation of the interposition mechanism. preload_elf, preload_macho, preload_bsd, preload_msvc, preload_mingw, ptrace. Each backend self-registers via a constructor-section scan.

See also: ptrace

ptrace

The Linux kernel debugging primitive. retrace's ptrace backend uses it to intercept calls in statically-linked binaries that LD_PRELOAD can't reach. Slower than preload; reserved for that case.

ABI

Application Binary Interface — the register/stack convention for passing arguments. Sys V x86-64, AAPCS64, Microsoft x64. retrace's trampolines are per-ABI.

AAPCS64

The AArch64 Procedure Call Standard. Integer args in x0–x7, FP args in v0–v7, return in x0 (or v0). Variadic args spill to the stack on Darwin but stay in registers on Linux.

Quick reference

The whole tool, on one page.

Screenshot this. Print it. Tape it to your monitor. Everything below is also in theCLI referenceand configuration reference, but here it is at a glance.

CLI

Ten subcommands

  • trace [funcs]Log calls (optionally --html)
  • mock <fn> <ret>Override a return value
  • fuzz [<fn>] --rate RInject OOM at rate R
  • slow <fn> --ms NInject N ms latency
  • run --config FJSON config-driven
  • pp <log.json>Pretty-print a trace
  • html <log.json>Convert log to HTML
  • list-functionsEnumerate interceptable
  • list-actionsEnumerate actions
  • validate <cfg.json>Check a config
Actions

Twelve built-in

  • log_params(omit_params)
  • call_real
  • fuzzing_seedseed
  • modify_in_param_strparam_name, new_str, match_str?
  • modify_in_param_intparam_name, new_int, match_int?
  • modify_in_param_arrparam_name, new_arr, match_arr?
  • modify_return_value_intretval_int
  • delayms
  • memory_fuzzfail_rate
  • incomplete_iorate
  • call_count_limitlimit
  • sandboxdeny_paths
Env vars

Seven tunables

  • RETRACE_JSON_CONFIGPath to JSON config
  • RETRACE_LIBOverride library path
  • RETRACE_LOGGER_DEF_FNLog file path
  • RETRACE_LOGGER_DEF_ENA0 = disable logging
  • RETRACE_LOGGER_DEF_STDOUT_ENA0 = suppress stdout
  • RETRACE_LOGGER_ALLOWED_FUNCSComma allowlist
  • RETRACE_LOGGER_EXCLUDED_FUNCSComma denylist
Patterns

Three config recipes

Trace + run
{
  "func_name": "*",
  "actions": [
    { "action_name": "log_params" },
    { "action_name": "call_real" }
  ]
}
Rewrite + run
{
  "func_name": "open",
  "actions": [
    { "action_name": "modify_in_param_str",
      "action_params": {
        "param_name": "path",
        "new_str": "/tmp/fake"
      } },
    { "action_name": "call_real" }
  ]
}
Fail after N
{
  "func_name": "malloc",
  "actions": [
    { "action_name": "modify_return_value_int",
      "action_params": { "retval_int": 0 } },
    { "action_name": "call_count_limit",
      "action_params": { "limit": 5 } },
    { "action_name": "call_real" }
  ]
}