Kernel-Echte Foutinjectie: Een C99 LD_PRELOAD Chaos-bibliotheek voor Elk Proces
Engineering category.testing 26 juni 2026

Kernel-Echte Foutinjectie: Een C99 LD_PRELOAD Chaos-bibliotheek voor Elk Proces

Zes foutdomeinen, nul wijzigingen in applicatiecode, elk ELF-binair bestand. Een C99 chaos-bibliotheek die libc-aanroepen onderschept op het niveau van de dynamische linker — waarmee kernel-echte fouten worden geleverd aan Java, Go, Python, Node.js en alles daartussenin.

E
Engineering Team
Senior Solutions Architects
16 min lezen

Het Probleem met het Mocken van Systeemaanroepen

Foutinjectiebibliotheken vallen uiteen in twee brede kampen. Het eerste kamp mockt op de applicatielaag: een repository mocken, een HTTP-client mocken, een uitzondering teruggeven. Overzichtelijk. Snel. En volledig losgekoppeld van wat de echte operationele omgeving doet wanneer die degradeert.

Het tweede kamp grijpt naar infrastructuurtools: Toxiproxy voor TCP-proxykopieën, tc netem voor pakketvormgeving op kernelniveau, Pumba of Chaos Monkey voor het uitschakelen van containers. Krachtig, maar ze vereisen externe opzet, botsen met testisolatie en draaien vaak in een apart proces waarvan de applicatie geen weet kan hebben.

Beide kampen missen iets: de libc-grens. De laag waar uw applicatie, ongeacht taal of runtime, overdraagt aan het besturingssysteem. Waar `read()` daadwerkelijk plaatsvindt. Waar `getaddrinfo()` namen omzet. Waar `mmap()` geheugen alloceert. Waar `clock_gettime()` de tijd retourneert.

Als u hier onderschept, onderschept u alles. En als de fouten die u op deze laag injecteert niet te onderscheiden zijn van de fouten die de kernel produceert — omdat ze dezelfde errno-codes gebruiken, dezelfde retourwaardeconventies, dezelfde timingkenmerken — dan is wat u heeft gebouwd geen mock. Het is een simulatie. Een kernel-echte simulatie.

Waarom de libc-grens de Juiste Laag Is

LD_PRELOAD is een functie van de Linux dynamische linker die gedeelde objecten in de linkmap van een proces invoegt vóór elke andere bibliotheek — inclusief libc. Wanneer een applicatie `connect()` aanroept, doorloopt de symboolresolutie van de dynamische linker de linkmap in volgorde. Als een vooraf geladen bibliotheek een symbool met de naam `connect` exporteert, wint dat symbool.

Dit geeft ons interpositie: de mogelijkheid om elke libc-functie te omhullen zonder de aanroepende code te wijzigen of opnieuw te compileren. En het is beschikbaar sinds voordat de meeste huidige productiesystemen werden ontworpen.

De belangrijkste eigenschappen die dit de juiste laag maken:

**Taalonafhankelijke universaliteit.** Python's `socket.connect()` loopt via glibc. Node.js's `net.connect()` loopt via glibc. Java's `Socket.connect()` — via JNI — loopt via glibc. Een enkele vooraf geladen bibliotheek onderschept ze allemaal.

**Kernel-echte fouten.** We retourneren echte errno-waarden. De aanroepende code kan een echte ECONNREFUSED niet onderscheiden van onze geïnjecteerde ECONNREFUSED. De circuitonderbreker ziet een echte storing, geen testbewuste gesimuleerde.

**Nul applicatiewijzigingen.** Geen agents. Geen API-imports. Geen testframework-integratie vereist op applicatieniveau. De applicatie draait normaal; de fouten verschijnen eronder.

**Procesbereik.** Fouten zijn beperkt tot een enkel proces. Voer uw testproces uit met één set fouten; voer uw afhankelijkheid uit in een aparte container zonder fouten. Geen kruisbesmetting.

Zes Foutdomeinen

De bibliotheekset dekt zes onafhankelijke foutdomeinen, elk als een apart gedeeld object. Ze kunnen afzonderlijk worden geladen of worden samengesteld — de samenstelling is veilig omdat elke bibliotheek een disjuncte set symbolen bezit (het Symboolbezitstheorema, gedetailleerd gedocumenteerd in de architectuurdocumentatie).

text
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)

Symboolinterpositie: Hoe Het Daadwerkelijk Werkt

Het mechanisme is eenvoudig in principe, zorgvuldig in uitvoering.

**Stap 1 — Bibliotheekinitialisatie.** Wanneer het vooraf geladen `.so` wordt geladen, wordt de constructor uitgevoerd. Het roept `dlsym(RTLD_NEXT, "connect")` aan om de echte libc `connect` te vinden. RTLD_NEXT slaat de aanroepende DSO in de linkmap over en retourneert het volgende overeenkomende symbool — de echte libc-implementatie. Deze aanwijzer wordt gecached in een statische variabele. Geen per-aanroep dlsym in het hot path.

**Stap 2 — Aanroeponderbreking.** Wanneer de applicatie `connect()` aanroept, vindt de dynamische linker eerst ons symbool (linkmapvolgorde). Onze wrapper wordt uitgevoerd: het leest het configuratiebestand (of een gecachede momentopname), evalueert of een fout moet worden geactiveerd op basis van de eindpuntselector en kans, en injecteert de fout of roept de echte libc-functie aan via de gecachede aanwijzer.

**Stap 3 — Symboolbezitsinvariant.** Als twee vooraf geladen bibliotheken beide `read()` proberen te omhullen, zal één van hen RTLD_NEXT krijgen dat naar de andere wijst, wat dubbele injectie veroorzaakt. We voorkomen dit met strikte symbooloewijzing: elk libc-symbool wordt eigendom van precies één bibliotheek in de set. `libchaos-net.so` bezit `read()` niet — I/O-symbolen behoren toe aan `libchaos-io.so`. De bibliotheken zijn veilig samen te stellen omdat ze daarvoor zijn ontworpen.

bash
# 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

De Configuratiebestandsinterface

Er is geen API. Er is geen publieke header. Er is geen bibliotheek om tegen te linken. De volledige interface is een plat tekstconfiguratiebestand.

Dit was een bewuste keuze. Een configuratiebestand kan worden geschreven door een shellscript, een CI-taak, een testharnas in elke taal, of een mens in een terminal. Het vereist geen integratie met het bouwsysteem. Het kan worden verwisseld terwijl het proces actief is. Het kan worden versiebeheerd naast testdata. En het creëert nul koppeling tussen de chaos-bibliotheek en de te testen applicatie.

De configuratiesyntaxis is `selector:operation:effect:value:probability`. Een paar representatieve voorbeelden:

text
# 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

Vergrendelingsvrije Configuratieherlaad tijdens Runtime

Een van de minder voor de hand liggende technische uitdagingen is het herladen van de configuratie terwijl een multi-threaded proces actief is. De naïeve aanpak — het bestand bij elke aanroep lezen — is te duur. De naïeve vergrendelingsgebaseerde aanpak — een mutex vasthouden tijdens het bijwerken van de configuratiemomentopname — blokkeert elke onderschepte systeemaanroep tijdens het herladen.

We gebruiken een vergrendelingsvrije aanpak gebaseerd op atomaire vergelijking-en-verwisseling:

**Twee momentopnamebuffers.** Eén is actief; de andere is beschikbaar voor schrijven. Een 64-bit atomaire sleuf bevat de mtime van de momenteel actieve configuratie.

**Herlaadwinnaardetectie.** Elke onderbreking controleert de mtime van het configuratiebestand ten opzichte van de gecachede waarde. Bij een verschil wint één thread de CAS-race om te beginnen met herladen. Verliezers zien de RELOADING-toestand en wachten kort.

**Atomaire omschakeling.** De winnaar schrijft de nieuwe configuratie naar de inactieve buffer en wisselt vervolgens atomair de actieve index. Vanaf dit punt zien alle threads de nieuwe configuratie. De oude buffer is beschikbaar voor de volgende herlaadcyclus.

Het herlaadvenster is typisch minder dan 15 microseconden. Lezers worden nooit geblokkeerd door schrijvers in de stationaire toestand. Dit maakt het praktisch om foutpercentages te wijzigen tussen testmethoden zonder het proces opnieuw te starten.

CI-integratie: Drie Regels YAML

Integratie in een CI-pipeline is bewust minimaal gehouden. Vooraf gecompileerde binaire bestanden voor vier platformcombinaties (glibc/musl × amd64/arm64) zijn beschikbaar. Geen bouwstap nodig — download het juiste binaire bestand voor het besturingssysteem en de architectuur van uw CI-runner, schrijf een configuratiebestand en stel LD_PRELOAD in.

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-lokale Internals: Geen Globale Toestand

Multi-threaded correctheid wordt afgedwongen via thread-lokale opslag in plaats van vergrendelingen waar mogelijk.

**PRNG per thread.** Elke thread heeft zijn eigen toestand voor de generator van willekeurige getallen. Foutsequenties zijn onafhankelijk per thread — geen globale willekeurigheidsknelpunt, geen contentie op gedeelde toestand. Dit betekent ook dat foutsequenties deterministisch per thread zijn bij hetzelfde zaad, wat van belang is voor het reproduceren van testfouten.

**Recursiewachter.** Wanneer de chaoscode libc-internals aanroept (stat om mtime te controleren, read om het configuratiebestand te lezen), zouden die aanroepen zelf worden onderschept, wat oneindige recursie veroorzaakt. Een `__thread int guard`-variabele voorkomt dit: als de recursidiepte niet nul is, roepen we onmiddellijk door naar de echte functie.

**FD-naar-pad-cache.** De onderschepper voor bestands-I/O moet een bestandsdescriptor koppelen aan een pad (voor het matchen van padprefixregels). Het lezen van `/proc/self/fd/<fd>` bij elke I/O-aanroep is te duur. We onderhouden een 32-sleuf direct-mapped thread-lokale cache (fd % 32) die wordt gevuld bij `open()` en ongeldig gemaakt bij `close()`. Het cachemispercentage voor echte werklasten is onder de 3%.

Wat Wel en Niet Kan Worden Onderschept

We documenteren de beperkingen expliciet in plaats van gebruikers ze te laten ontdekken.

**Kan niet worden onderschept:**

- **Directe systeemaanroepen.** De Go-runtime doet systeemaanroepen rechtstreeks via het `syscall`-pakket, waarbij libc volledig wordt omzeild. Go-programma's gecompileerd zonder CGO zijn immuun voor LD_PRELOAD-chaos op hun netwerk-I/O. (Java via JNI is prima; JNI loopt via glibc.) - **Statische binaire bestanden.** LD_PRELOAD is een dynamische linkermechanisme. Statische binaire bestanden hebben geen dynamische linker; vooraf laden heeft geen effect. - **Setuid/setgid binaire bestanden.** De kernel verwijdert LD_PRELOAD voor setuid-binaire bestanden (AT_SECURE-semantiek). Dit is een beveiligingsfunctie waarop we terecht vertrouwen. - **vDSO fast-path klokken.** Sommige clock_gettime-aanroepen lopen via de vDSO, niet glibc. Onze tijdbibliotheek onderschept op het niveau van de glibc-wrapper, die stroomafwaarts van de vDSO zit. In de praktijk betekent dit dat de fout wordt geactiveerd op de glibc-aanroep vóór de vDSO-opzoekactie — het netto-effect is hetzelfde, maar de interne klok van de kernel wordt niet beïnvloed.

**Kan worden onderschept:** - Elk dynamisch gelinkt proces op Linux (glibc of musl) - Java via JNI — inclusief alle socket-, bestands-, DNS- en tijdaanroepen in de JDK - Python, Node.js, Ruby, PHP — gebruiken allemaal glibc voor systeem-I/O - Rust-binaire bestanden die std gebruiken (wat glibc gebruikt) of CGO-overbrugde C-bibliotheken

  • Java (JNI-pad): volledige dekking — netwerk, DNS, bestand, klok, geheugen
  • Python / Node.js / Ruby: volledige dekking via glibc
  • Go (CGO=1 of C-bibliotheken): gedeeltelijke dekking — CGO-aanroepen onderschept, pure Go-systeemaanroepen omzeilen
  • Go (puur, CGO=0): geen dekking — directe systeemaanroep-ABI, LD_PRELOAD heeft geen effect
  • Statische binaire bestanden: per definitie geen dekking

Prestaties: De Overheadcijfers

We meten doorgaande overhead met rdtsc-geserialiseerde microbenchmarks om worst-case cijfers te verkrijgen. Het hot path voor een doorgaande aanroep (geen fout geconfigureerd):

- **Configuratiecache-treffer (geen herlaad nodig):** ~20–40 ns overhead boven de werkelijke systeemaanroepkosten. Dit is een mtime-statcontrole (gecached in een atomaire sleuf), een regelscanning (lineair over N regels, typisch N < 16) en een aanroep via de gecachede functieaanwijzer. - **Cachemis (configuratiebestand herladen):** Één stat()-systeemaanroep plus een read() van het configuratiebestand — typisch 150–300 ns totaal, en alleen bij de eerste aanroep na een configuratiewijziging. - **Fout geïnjecteerd:** De foutoverhead zelf (slapen voor LATENCY, errno instellen voor ERRNO) domineert; de wrapperkosten zijn verwaarloosbaar.

Voor een goed geconfigureerde testsuite met een klein aantal regels ligt de doorgaande overhead onder de ruis van een netwerkaanroep. U betaalt tientallen nanoseconden bovenop bewerkingen die microseconden tot milliseconden kosten.

Key Takeaways

De C99 LD_PRELOAD chaos-bibliotheek is hoe kernel-echte foutinjectie eruitziet wanneer deze is ontworpen vanuit eerste principes in plaats van samengesteld uit bestaande tools.

Zes onafhankelijke foutdomeinen. Disjunct symboolbezit zodat ze veilig kunnen worden samengesteld. Een platte tekstconfiguratie-interface die werkt vanuit elke taal of toolchain. Vergrendelingsvrij hot reload zodat foutpercentages kunnen worden gewijzigd tussen testmethoden. Thread-lokale PRNG zodat foutsequenties onafhankelijk en deterministisch zijn. Vooraf gecompileerde binaire bestanden voor glibc en musl op amd64 en arm64.

En 100% regelafdekking afgedwongen in CI, omdat we genoeg software hebben verzonden op cartridges zonder patchknoppen om te weten dat elke ongedekte regel een productie-incident is dat staat te wachten.

De broncode is beschikbaar op chaos-testing-libraries. Het Java-testframework dat deze bibliotheken automatisch in uw Docker-containers injecteert, is te vinden op chaos-testing.

#chaos-engineering #c99 #ldpreload #elf #libc #fault-injection #ci-cd #linux #resilience
E

Engineering Team

Senior Solutions Architects

We bouwen al gedistribueerde systemen voordat 'microservices' een begrip was. Onze littekens vertellen verhalen.