Skip to content

Repository files navigation

retrace

Build Alpine MSYS2 OHOS FreeBSD Nix checkpatch Coverity Scan

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.

Supported platforms (v2.1.0)

OS Architecture Interposition Status

Linux (glibc)

x86_64, aarch64

LD_PRELOAD

Production

Linux (musl / Alpine)

x86_64, aarch64

LD_PRELOAD

Production

Linux (musl / OHOS)

aarch64

LD_PRELOAD

Cross-compile + signed

macOS

arm64 (Apple Silicon)

DYLD_INSERT_LIBRARIES

Production (printf fixed in v2.1.0)

macOS

x86_64 (Intel)

DYLD_INSERT_LIBRARIES

Production (init + FP varargs fixed in v2.1.0)

FreeBSD / OpenBSD / NetBSD

x86_64

LD_PRELOAD

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

ptrace(2)

Production

Pre-built binaries for every platform are attached to each release.

What’s new in v2.1.0

  • macOS Intel fully working. ld64 on Intel macOS silently drops the .quad _<name> reference in DATA,retrace_rimpls for malloc and realloc, leaving retrace_real_impls_init failing 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..xmm7 on entry and restores them before the tail-call to the real libc function. Sys V passes variadic FP args in xmm registers; previously printf("%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 under DYLD_INSERT_LIBRARIES on Apple Silicon. retrace now reads variadic args from the caller’s stack frame on Darwin AArch64 (the Apple ABI puts them there, not in x1..x7 like 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_format is implemented locally where musl doesn’t ship printf.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, sets LD_PRELOAD / DYLD_INSERT_LIBRARIES and the RETRACE_* 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 is uid=0. The previous smoke step ran /usr/bin/id which is SIP-protected on macOS — DYLD_INSERT_LIBRARIES was stripped and the test passed without proving interception happened.

  • Single-library install. The _v2 suffix on the library name was dropped (v1 source was removed per ADR-0011). Install produces libretrace.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.

Build

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_BUILD_V2

ON — build the retrace shared library (libretrace.so / .dylib / .dll).

RETRACE_BUILD_CLI

ON — build the CLI launcher (placeholder; full CLI in a future release).

RETRACE_BUILD_TESTS

OFF — build per-feature test binaries under test/.

RETRACE_BUILD_EXAMPLES

OFF — build the demos under examples/.

RETRACE_ENABLE_RPC

OFF — build the optional RPC subsystem under rpc/.

RETRACE_ENABLE_ASAN / _UBSAN / _TSAN / _COVERAGE

OFF — instrumentation toggles.

Running

$ 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

RETRACE_JSON_CONFIG

Path to a JSON config file (see below). If unset, retrace uses a built-in default that activates log_params + call_real for every intercepted function.

RETRACE_LOGGER_DEF_ENA

0 disables the logger entirely.

RETRACE_LOGGER_DEF_STDOUT_ENA

0 keeps retrace’s log off stdout so it doesn’t mix with the target’s own output.

RETRACE_LOGGER_DEF_FN

Path to a log file (alternative to stderr).

Note

On macOS, System Integrity Protection strips DYLD_INSERT_LIBRARIES for binaries in system directories. Run csrutil disable and reboot to trace them.

JSON configuration

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.

Built-in actions

Action Effect Required params

log_params

Log the call (function name + arguments) to the configured logger.

none

call_real

Invoke the real libc implementation. Omit this action to skip the real call entirely (return zero / NULL).

none

modify_in_param_str

Rewrite a string argument before the real call runs.

param_name, new_str; optional match_str to gate the rewrite on the current value

modify_in_param_int

Rewrite an integer argument before the real call runs.

param_name, new_int; optional match_int to gate the rewrite

modify_in_param_arr

Rewrite an array argument before the real call runs.

param_name, new_arr (see src/core/actions/basic.c for layout)

modify_return_value_int

Override the integer return value (after call_real if present).

retval_int

memory_fuzz

Randomly fail malloc / realloc / calloc (returns NULL). Useful for finding unchecked allocator returns.

fail_rate (float, 0..1)

New behaviors are added by registering a new action — no engine change required (see src/core/actions/basic.c for the pattern).

Intercepted functions

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

ctype.h

28

isalpha, isdigit, tolower, toupper, …​

dirent.h

13

opendir, readdir, closedir, …​

locale.h

4

setlocale, localeconv, …​

signal.h

5

kill, signal, raise, …​

stdio.h

165

fopen, fclose, fread, fwrite, printf, fprintf, sprintf, snprintf, dprintf, vprintf, vfprintf, vsprintf, vsnprintf, vdprintf, scanf, fscanf, sscanf, vscanf, vsscanf, vfscanf, fgets, perror, popen, …​

stdlib.h

73

malloc, calloc, realloc, free, getenv, system, exit, atoi, strtol, …​

uio.h

8

readv, writev, …​

unistd.h

198

read, write, open, close, fork, execve, getuid, getpid, getenv, socket, connect, recv, send, …​

\ 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 (%f, %g, %e) on AArch64 are currently bailed out to a "named args only" fallback (see src/core/printf_compat.h). The integer/pointer/string conversions all work; FP-on-AArch64 needs the SIMD-reg save-area walk that is on the roadmap (ADR-0010).

Examples

Log every libc call (the default)

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

Redirect getenv("TEST") to getenv("PATH")

{
  "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" }
      ]
    }
  ]
}

Force getuid() to return 42

{
  "intercept_scripts": [
    {
      "func_name": "getuid",
      "actions": [
        { "action_name": "call_real" },
        { "action_name": "modify_return_value_int", "action_params": { "retval_int": 42 } }
      ]
    }
  ]
}

Fuzz malloc (10% failure rate)

{
  "intercept_scripts": [
    {
      "func_name": "malloc",
      "actions": [
        { "action_name": "call_real" },
        { "action_name": "memory_fuzz", "action_params": { "fail_rate": 0.1 } }
      ]
    }
  ]
}

Fuzzing-only mode (no log noise)

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-program

2. 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. */
  ]
}

End-to-end examples under examples/

Directory What it demonstrates

examples/dns-fuzz/

Fuzz DNS resolution paths

examples/getenv-fuzzing/

Buffer-overflow and format-string fuzzing of getenv

examples/http-server-overflow/

HTTP server input fuzzing

examples/id-redirection/

Redirect getuid/geteuid to fake privileges

examples/net-fuzzing/

Network call fuzzing

examples/stringinject/

File/string injection helper (paired with tools/stringinjector)

examples/unsafe-system/

Trace system() invocation paths

Architecture

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 retrace_engine_wrapper(func_name, frame), then either tail-calls the real implementation (if the engine set call_real_flag) or returns the synthesized ret_val. Lives under src/backends/preload_*/{x86_64,aarch64}/arch_spec_top.S.

Engine

retrace_engine_wrapper (in src/core/engine.c) is the central dispatch. Per-thread struct ThreadContext holds the prototype, real impl pointer, and parsed params. The engine looks up the matching intercept_script and runs its actions in order.

Action registry

Built-in actions live in src/core/actions/{basic,memfuzz}.c and self-register into a linker section the engine scans at init. Adding a new behavior = adding a new action file; no engine change.

Backend plugin system

Each (OS, arch) combo has its own backend (preload_elf, preload_macho, preload_bsd, preload_msvc, preload_mingw, ptrace). Backends self-register via a constructor-section scan; retrace_backend_select() picks the highest-rank backend whose probe() succeeds. See include/retrace/backend.h.

Real-impl indirection

All internal libc usage inside retrace goes through retrace_real_impls.<fn> function pointers, resolved once at init via dlsym(RTLD_NEXT, …​). This is the reentrancy guard — bypass it and you recurse.

Init order matters (see src/core/main.c constructor): retrace_as_initretrace_real_impls_initretrace_logger_init → parson alloc hooks → retrace_conf_initretrace_loger_update_configretrace_engine_initretrace_funcs_initretrace_datatypes_initretrace_actions_initretrace_as_init_late.

Architecture Decision Records under docs/adr/ capture the load-bearing decisions:

ADR

Topic

0006

Semantic versioning

0008

Opaque public types for ABI stability

0009

From-scratch Windows inline-hooking (no MinHook / Detours)

0010

AArch64 float params supported from day one

0011

v1 source removed at v2.1.0 (supersedes 0005)

Migrating from v1

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

libretrace.so (single library, per-function C wrappers)

libretrace.so (single library, single assembly trampoline per function + JSON-driven engine)

retrace shell-script CLI launcher

LD_PRELOAD / DYLD_INSERT_LIBRARIES directly. A native CLI is on the roadmap.

RETRACE_CONFIG=<file> (line-oriented text config)

RETRACE_JSON_CONFIG=<file.json> (JSON; default config activates log_params + call_real for *)

getuid,0 (text: func,value)

{ "func_name": "getuid", "actions": [{"action_name":"modify_return_value_int", "action_params":{"retval_int":0}}] }

fopen,/etc/passwd,/tmp/passwd

{ "action_name": "modify_in_param_str", "action_params": {"param_name":"path", "match_str":"/etc/passwd", "new_str":"/tmp/passwd"} }

connect,src_ip,src_port,dst_ip,dst_port

Use modify_in_param_arr on the sockaddr argument (the v1 line had no exact equivalent — v2’s action is more general).

SSL_get_verify_result,10

{ "action_name": "modify_return_value_int", "action_params": {"retval_int":10} }

memoryfuzzing,0.05

{ "action_name": "memory_fuzz", "action_params": {"fail_rate":0.05} }

incompleteio,10

Not yet ported to v2’s action system. Track via issue tracker if you need it.

fuzzingseed,1498729252

Not yet ported to v2. The memory_fuzz action uses rand() seeded from the PID; explicit seeding is on the roadmap.

logtofile,retrace.log

RETRACE_LOGGER_DEF_FN=retrace.log

logging-global,…​ / logging-excluded-funcs,…​ / logging-allowed-funcs,…​

Use multiple intercept_scripts entries with explicit func_name globs (allowlist model). Per-group log levels are not yet ported.

showtimestamp / showcalltime,0.0001

Not yet ported to v2. On the roadmap.

Autotools build: ./configure --enable-v2 && make

CMake: cmake -B build && cmake --build build

RETRACE_CLI=1 (interactive pty menu)

Removed. Will be replaced by the native CLI when it lands.

v1 examples under examples/*/retrace.conf

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 retrace shell-script launcher. Use LD_PRELOAD until the native CLI lands.

  • The RETRACE_CONFIG text-format config file. Use RETRACE_JSON_CONFIG.

  • The interactive pty CLI (RETRACE_CLI=1).

What was renamed:

  • The installed library is libretrace.so / libretrace.dylib / retrace.dll (was libretrace_v2.* briefly during the transition; the _v2 suffix was dropped at v2.1.0 because v1 no longer exists).

Known gaps (tracking in GitHub issues)

Platform / feature Status

dlopen under LD_PRELOAD on Linux

#450: malloc path inside libdl recurses / reenters retrace; skipped in CI.

Float varargs (%f, %g, %e)

Engine bails to asm Path A (correct output, no log_params entry for the call). Integer, pointer, and string conversions work end-to-end including log_params. Full SIMD-reg dispatch is on the roadmap (ADR-0010).

sprintf / snprintf / fprintf / dprintf interception

Added in v2.1.0 (PR #469). All printf-family + v*printf variants are now in the prototype registry.

scanf family interception

Added in v2.1.0 (PR #470). scanf / fscanf / sscanf / vscanf / vsscanf / vfscanf plus glibc _isoc99* variants.

Incomplete-I/O action

v1’s incompleteio,10 has no v2 action equivalent yet.

Explicit fuzzing seed

v1’s fuzzingseed,1498729252 is not yet ported. memory_fuzz uses rand() seeded from the PID.

Who is Ribose?

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.

Contact

  • Security issues, feature requests, and bug reports: GitHub Issues

  • General questions: retrace@ribose.com

About

retrace is a versatile security vulnerability / bug discovery tool through monitoring and modifying the behavior of compiled binaries on Linux, OpenBSD/FreeBSD/NetBSD (shared object) and macOS (dynamic library).

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages