retrace is a userspace security and vulnerability discovery tool that
intercepts libc calls in dynamically-linked binaries. It works by
preloading a shared library into the target process
(LD_PRELOAD on ELF, DYLD_INSERT_LIBRARIES on Darwin, inline
hooking on Windows) and either logging or rewriting each intercepted
call’s arguments and return value.
Use cases: reverse engineering, debugging, fuzzing (malloc failure
injection, getenv buffer-overflow / format-string / garbage fuzzing,
incomplete I/O), redirecting network connect() calls, redirecting
file open() paths, faking OpenSSL verify results, and more.
| OS | Architecture | Interposition | Status |
|---|---|---|---|
Linux (glibc) |
x86_64, aarch64 |
|
Production |
Linux (musl / Alpine) |
x86_64, aarch64 |
|
Production |
Linux (musl / OHOS) |
aarch64 |
|
Cross-compile + signed |
macOS |
arm64 (Apple Silicon) |
|
Production (printf fixed in v2.1.0) |
macOS |
x86_64 (Intel) |
|
Production (init + FP varargs fixed in v2.1.0) |
FreeBSD / OpenBSD / NetBSD |
x86_64 |
|
Production |
Windows (MSVC) |
x86_64, arm64 |
Inline hook (from scratch, ADR-0009) |
Production |
Windows (MinGW) |
x86_64 |
Inline hook |
Production |
Linux (statically linked) |
x86_64, aarch64 |
|
Production |
Pre-built binaries for every platform are attached to each release.
-
macOS Intel fully working. ld64 on Intel macOS silently drops the
.quad _<name>reference inDATA,retrace_rimplsfor malloc and realloc, leavingretrace_real_impls_initfailing on the malloc lookup and every wrapper call falling through to real libc. v2.1.0 adds a dlsym(RTLD_NEXT, …) fallback when the section walk misses, so init completes. Intel macOS joins Apple Silicon as a fully-supported platform. -
FP varargs on x86_64. The x86_64 trampoline now saves
xmm0..xmm7on entry and restores them before the tail-call to the real libc function. Sys V passes variadic FP args in xmm registers; previouslyprintf("%f", 1.5)produced garbage because the engine call clobbered them. Now correct on Linux, BSD, and macOS Intel. -
printf variadic dispatch on Apple Silicon.
printf("%d %s", …)no longer segfaults underDYLD_INSERT_LIBRARIESon Apple Silicon. retrace now reads variadic args from the caller’s stack frame on Darwin AArch64 (the Apple ABI puts them there, not inx1..x7like AAPCS64 does on Linux/BSD). All printf-family tests pass on M1/M2/M3. -
OHOS support. Cross-compile path for HarmonyOS / OpenHarmony arm64, with NDK + binary-sign-tool integration in CI.
-
Alpine / musl fully green.
parse_printf_formatis implemented locally where musl doesn’t shipprintf.h; integer printf/scanf work end-to-end on Alpine x86_64 and aarch64. -
Native CLI launcher (
src/cli/retrace_cli.c) replaces the v1 shell script. Auto-detects the library path, setsLD_PRELOAD/DYLD_INSERT_LIBRARIESand theRETRACE_*env vars, execs the target. POSIX-only — Windows uses CreateRemoteThread injection. -
Smoke test verifies interception in CI: builds a tiny
getuid()binary, redirects via JSON config, asserts the output isuid=0. The previous smoke step ran/usr/bin/idwhich is SIP-protected on macOS —DYLD_INSERT_LIBRARIESwas stripped and the test passed without proving interception happened. -
Single-library install. The
_v2suffix on the library name was dropped (v1 source was removed per ADR-0011). Install produceslibretrace.so/libretrace.dylib/retrace.dll. -
Per-platform binary artifacts in every release: 8 platforms (Linux x64/arm64, macOS x64/arm64, Windows x64/arm64, Alpine x64/arm64, OHOS arm64) + source tarball.
retrace uses CMake. vcpkg manifest mode pulls OpenSSL and cmocka on Windows automatically; system packages provide them on POSIX.
$ cmake -B build -G Ninja -DRETRACE_BUILD_TESTS=ON
$ cmake --build build
$ ctest --test-dir build --output-on-failure
$ sudo cmake --install build|
Tip
|
Quick smoke test without installing: # Linux / BSD
LD_PRELOAD=$PWD/build/src/v2/libretrace.so /bin/id
# macOS (Apple Silicon)
DYLD_INSERT_LIBRARIES=$PWD/build/src/v2/libretrace.dylib /bin/id
# Windows (PowerShell, after install)
$env:RETRACE_JSON_CONFIG = "config.json"
myapp.exe # retrace.dll is auto-attached via CreateProcess hook |
Useful CMake options:
Option |
Default / purpose |
|
|
|
|
|
|
|
|
|
|
|
|
$ RETRACE_JSON_CONFIG=<config.json> LD_PRELOAD=build/src/v2/libretrace.so <binary>On macOS replace LD_PRELOAD with DYLD_INSERT_LIBRARIES:
$ RETRACE_JSON_CONFIG=<config.json> DYLD_INSERT_LIBRARIES=build/src/v2/libretrace.dylib <binary>Environment variables:
Variable |
Purpose |
|
Path to a JSON config file (see below). If unset, retrace uses a built-in default that activates |
|
|
|
|
|
Path to a log file (alternative to stderr). |
|
Note
|
On macOS, System Integrity Protection strips |
retrace is driven by a JSON config file. The top-level shape:
{
"intercept_scripts": [
{
"func_name": "<glob or exact symbol>",
"actions": [
{ "action_name": "<action>", "action_params": { ... } },
...
]
},
...
]
}A func_name of "*" matches every intercepted symbol. Otherwise
the name must match a libc symbol retrace knows about (see
src/core/prototypes/ for the canonical list, grouped by header:
stdio.c, stdlib.c, unistd.c, dirent.c, uio.c, signal.c,
ctype.c, locale.c).
Actions run in the order listed. Each action either observes the call, mutates an argument, mutates the return value, or skips the real call entirely.
| Action | Effect | Required params |
|---|---|---|
|
Log the call (function name + arguments) to the configured logger. |
none |
|
Invoke the real libc implementation. Omit this action to skip the real call entirely (return zero / NULL). |
none |
|
Rewrite a string argument before the real call runs. |
|
|
Rewrite an integer argument before the real call runs. |
|
|
Rewrite an array argument before the real call runs. |
|
|
Override the integer return value (after |
|
|
Randomly fail |
|
New behaviors are added by registering a new action — no engine change
required (see src/core/actions/basic.c for the pattern).
retrace ships ~490 prototypes grouped by libc header. The canonical
list lives under src/core/prototypes/. Add a function by adding a
struct FuncPrototype entry to the right header file (and a
WRAPPER_ENTRY_* line in the matching funcs_symbols.S).
| Header | Count | Notable symbols |
|---|---|---|
|
28 |
|
|
13 |
|
|
4 |
|
|
5 |
|
|
165 |
|
|
73 |
|
|
8 |
|
|
198 |
|
\ Variadic.* printf/scanf-family prototypes are tagged FAT_PRINTF /
FAT_SCANF and go through the variadic-aware dispatcher so they work
on every platform’s ABI:
-
Apple AArch64: variadic args are pushed onto the caller’s stack (Apple’s ABI).
-
Linux/BSD AArch64 (AAPCS64): variadic args go in
x1..x7, then the stack. -
x86-64 (Sys V / Darwin): variadic args go in
rdi..r9, then the stack.
On glibc, modern gcc redirects scanf / fscanf / sscanf / vscanf
/ vsscanf / vfscanf to isoc99_* at the PLT. retrace intercepts
both the plain names (older binaries, musl, BSD) and the isoc99_*
names (modern glibc binaries) when the CMake link-check confirms the
symbol exists.
|
Tip
|
Float varargs ( |
{
"intercept_scripts": [
{
"func_name": "*",
"actions": [
{ "action_name": "log_params" },
{ "action_name": "call_real" }
]
}
]
}{
"intercept_scripts": [
{
"func_name": "getenv",
"actions": [
{ "action_name": "log_params" },
{
"action_name": "modify_in_param_str",
"action_params": { "param_name": "name", "match_str": "TEST", "new_str": "PATH" }
},
{ "action_name": "call_real" }
]
}
]
}{
"intercept_scripts": [
{
"func_name": "getuid",
"actions": [
{ "action_name": "call_real" },
{ "action_name": "modify_return_value_int", "action_params": { "retval_int": 42 } }
]
}
]
}{
"intercept_scripts": [
{
"func_name": "malloc",
"actions": [
{ "action_name": "call_real" },
{ "action_name": "memory_fuzz", "action_params": { "fail_rate": 0.1 } }
]
}
]
}When you’re running memory_fuzz over a large program, the default
log_params + call_real for every libc call produces gigabytes of
JSON you don’t care about. Two ways to suppress it:
1. Disable the logger entirely (zero JSON output, fuzz still runs):
$ RETRACE_LOGGER_DEF_ENA=0 \
RETRACE_JSON_CONFIG=fuzz.json \
LD_PRELOAD=build/src/v2/libretrace.so ./your-program2. Allowlist only the function you’re fuzzing (log only malloc):
{
"intercept_scripts": [
{
"func_name": "malloc",
"actions": [
{ "action_name": "log_params" },
{ "action_name": "call_real" },
{ "action_name": "memory_fuzz", "action_params": { "fail_rate": 0.1 } }
]
}
/* NOTE: no "*" wildcard script -- every other libc call goes
* through retrace's trampoline but the engine finds no matching
* script and just calls real. No log noise. */
]
}| Directory | What it demonstrates |
|---|---|
|
Fuzz DNS resolution paths |
|
Buffer-overflow and format-string fuzzing of |
|
HTTP server input fuzzing |
|
Redirect |
|
Network call fuzzing |
|
File/string injection helper (paired with |
|
Trace |
retrace is built around five clean concepts. Each is a MECE module extensible without modifying the others (Open/Closed Principle).
Per-arch trampoline |
One hand-written assembly trampoline per function. Pushes the SysV /
Microsoft x64 / AArch64 PCS register arguments into a frame, calls
|
Engine |
|
Action registry |
Built-in actions live in |
Backend plugin system |
Each (OS, arch) combo has its own backend ( |
Real-impl indirection |
All internal libc usage inside retrace goes through
|
Init order matters (see src/core/main.c constructor):
retrace_as_init → retrace_real_impls_init → retrace_logger_init
→ parson alloc hooks → retrace_conf_init →
retrace_loger_update_config → retrace_engine_init →
retrace_funcs_init → retrace_datatypes_init →
retrace_actions_init → retrace_as_init_late.
Architecture Decision Records under docs/adr/ capture the
load-bearing decisions:
ADR |
Topic |
|
Semantic versioning |
|
Opaque public types for ABI stability |
|
From-scratch Windows inline-hooking (no MinHook / Detours) |
|
AArch64 float params supported from day one |
|
v1 source removed at v2.1.0 (supersedes |
v1’s source code was removed at the v2.1.0 release (ADR-0011). If you were running v1, the table below maps every v1 concept to its v2 equivalent.
v1 |
v2 |
|
|
|
LD_PRELOAD / DYLD_INSERT_LIBRARIES directly. A native CLI is on the roadmap. |
|
|
|
|
|
|
|
Use |
|
|
|
|
|
Not yet ported to v2’s action system. Track via issue tracker if you need it. |
|
Not yet ported to v2. The |
|
|
|
Use multiple |
|
Not yet ported to v2. On the roadmap. |
Autotools build: |
CMake: |
|
Removed. Will be replaced by the native CLI when it lands. |
v1 examples under |
Still in the tree as v1-format text configs. They will be ported to v2 JSON in a follow-up. |
What was removed in v2.1.0:
-
The entire v1 source tree (
src/v1/). -
The Autotools build system (
configure.ac,Makefile.am,m4/,autogen.sh,configure, etc.). CMake is the only build system. -
The
retraceshell-script launcher. UseLD_PRELOADuntil the native CLI lands. -
The
RETRACE_CONFIGtext-format config file. UseRETRACE_JSON_CONFIG. -
The interactive pty CLI (
RETRACE_CLI=1).
What was renamed:
-
The installed library is
libretrace.so/libretrace.dylib/retrace.dll(waslibretrace_v2.*briefly during the transition; the_v2suffix was dropped at v2.1.0 because v1 no longer exists).
| Platform / feature | Status |
|---|---|
|
#450: malloc path inside libdl recurses / reenters retrace; skipped in CI. |
Float varargs ( |
Engine bails to asm Path A (correct output, no |
|
Added in v2.1.0 (PR #469). All printf-family + v*printf variants are now in the prototype registry. |
|
Added in v2.1.0 (PR #470). |
Incomplete-I/O action |
v1’s |
Explicit fuzzing seed |
v1’s |
We are Ribose, the secure sharing company. We
believe privacy and security form the foundation of liberty. We created
retrace to aid developers and security researchers in building
better, more defensible software.
-
Security issues, feature requests, and bug reports: GitHub Issues
-
General questions:
retrace@ribose.com