The Problem with Mocking System Calls
Why the libc Boundary Is the Right Layer
Six Fault Domains
libchaos-io.so — File I/O
Hooks: open/openat, read/readv, write/writev, pread/preadv, pwrite/pwritev,
close, fsync/fdatasync, ftruncate, fallocate, unlinkat, renameat,
sendfile, copy_file_range
Effects: ERRNO (EIO, ENOSPC, EDQUOT, ...), LATENCY,
TORN writes (short return < requested), CORRUPT reads (bit flip)
libchaos-net.so — Network Sockets
Hooks: socket/socketpair, bind, listen, connect, accept,
send/sendto/sendmsg/sendmmsg, recv/recvfrom/recvmsg/recvmmsg,
poll, select, epoll_wait/epoll_pwait
Effects: ERRNO (ECONNREFUSED, ECONNRESET, ETIMEDOUT, ...), LATENCY, CORRUPT
libchaos-dns.so — DNS Resolution
Hooks: getaddrinfo, getnameinfo
Effects: Name rewrites, address synthesis, EAI_AGAIN, EAI_FAIL, EAI_NONAME,
EAI_MEMORY, answer shuffling, family filtering, result limiting
libchaos-time.so — Clock & Timing
Hooks: clock_gettime (all POSIX clocks), nanosleep, usleep
Effects: ERRNO, LATENCY, OFFSET (signed millisecond time skew)
libchaos-memory.so — Memory Allocation
Hooks: mmap, munmap, mprotect, madvise
Effects: ERRNO (ENOMEM, ENFILE), LATENCY
libchaos-process.so — Process Lifecycle
Hooks: pthread_create, fork, posix_spawn/posix_spawnp, execve/execveat,
waitpid
Effects: ERRNO, LATENCY, FAIL_AFTER (allow N calls then fail permanently)
Symbol Interposition: How It Actually Works
# Compose safely — net + dns + io loaded simultaneously, no double-injection
LD_PRELOAD=libchaos-net.so:libchaos-dns.so:libchaos-io.so ./your-service
# Each library reads its own config file independently
/tmp/.chaos-net.conf
/tmp/.chaos-dns.conf
/tmp/.chaos-io.conf
The Config File Interface
# libchaos-net.conf — network fault examples
#
# ECONNREFUSED on 10% of connects to the payments service
tcp4://payments.internal:8080:connect:ECONNREFUSED:0.10
#
# ECONNRESET on 5% of recv() on any TCP socket
tcp4://*:*:recv:ECONNRESET:0.05
#
# 200ms latency on 100% of connects to the analytics backend
tcp4://analytics.internal:9000:connect:LATENCY:200
#
# Corrupt 1% of sent packets to any endpoint (single bit flip)
tcp4://*:*:send:CORRUPT:0.01
# libchaos-dns.conf — DNS fault examples
#
# Transient failure for all *.internal lookups
dns://*.internal:EAI_AGAIN:0.20
#
# Permanent failure for a specific service
dns://legacy-payments.svc.cluster.local:EAI_FAIL:1.0
# libchaos-io.conf — filesystem fault examples
#
# Torn writes (short return) on WAL file
/data/wal:write:TORN:0.05
#
# EIO on 1% of reads from the config directory
/etc/app:read:EIO:0.01
Lock-Free Config Reload at Runtime
CI Integration: Three Lines of YAML
# GitHub Actions example: inject network chaos for integration tests
- name: Download libchaos
run: |
curl -fsSL https://github.com/macstab/chaos-testing-libraries/releases/latest/\
download/libchaos-net-glibc-amd64.so -o /tmp/libchaos-net.so
- name: Write chaos config
run: |
cat > /tmp/.chaos-net.conf << 'EOF'
tcp4://db.internal:5432:connect:ECONNREFUSED:0.10
tcp4://cache.internal:6379:connect:ETIMEDOUT:0.05
dns://*.internal:EAI_AGAIN:0.15
EOF
- name: Run integration tests with chaos
env:
LD_PRELOAD: /tmp/libchaos-net.so
run: ./gradlew integrationTest
Thread-Local Internals: No Global State
What Can and Cannot Be Intercepted
- Java (JNI path): full coverage — network, DNS, file, clock, memory
- Python / Node.js / Ruby: full coverage via glibc
- Go (CGO=1 or C libraries): partial coverage — CGO calls intercepted, pure Go syscalls bypass
- Go (pure, CGO=0): no coverage — direct syscall ABI, LD_PRELOAD has no effect
- Static binaries: no coverage by definition
Performance: The Overhead Numbers
tags
conclusion
Engineering Team
Senior Solutions Architects
We've been building distributed systems since before 'microservices' was a thing. Our scars tell stories.