skills/dynamic-analysis/SKILL.md (6736 bytes)
1 --- 2 name: dynamic-analysis 3 description: Find memory errors, undefined behaviour, data races, and parser crashes using sanitizers (ASan, UBSan, TSan, MSan, Miri) and coverage-guided fuzzing (libFuzzer, cargo-fuzz, Go native fuzzing, AFL++). Use when working in C, C++, Rust unsafe, Go, or Zig; when writing or modifying code that parses untrusted input; when debugging a segfault, heap corruption, use-after-free, flaky crash, or suspected data race; or when adding a new parser, decoder, deserialiser, or protocol handler. 4 --- 5 6 # Dynamic analysis 7 8 Static analysis proves absence of some bugs. Sanitizers and fuzzing find the 9 bugs that are actually there. If the code parses input you did not generate, 10 this is not optional. 11 12 ## Preflight 13 14 Check availability before writing anything; report clearly if missing. 15 16 ```bash 17 clang --version; command -v cargo-fuzz; go version; command -v afl-fuzz 18 rustup component list --toolchain nightly 2>/dev/null | grep -i miri 19 ``` 20 21 Sanitizers ship with clang and gcc. libFuzzer ships with clang. `cargo-fuzz` 22 needs `cargo install cargo-fuzz` and a nightly toolchain. Go fuzzing is built 23 into `go test`. 24 25 ## Sanitizers 26 27 Rebuild with instrumentation and run the existing test suite. This alone finds 28 real bugs in most untested C/C++ codebases. 29 30 ```bash 31 # ASan + UBSan together — the default pairing 32 clang -fsanitize=address,undefined -fno-omit-frame-pointer \ 33 -fno-sanitize-recover=all -g -O1 ... 34 ``` 35 36 `-fno-sanitize-recover=all` makes UBSan abort instead of printing and 37 continuing. Without it, UBSan findings scroll past in CI and get ignored. 38 39 **ASan and TSan cannot be combined.** They use incompatible shadow memory 40 layouts. Use separate build directories: 41 42 ```bash 43 cmake -B build-asan -DCMAKE_C_FLAGS="-fsanitize=address,undefined -g -O1" 44 cmake -B build-tsan -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" 45 ``` 46 47 | Sanitizer | Flag | Finds | Slowdown | 48 |---|---|---|---| 49 | ASan | `-fsanitize=address` | use-after-free, buffer overflow, double free, leaks | ~2x | 50 | UBSan | `-fsanitize=undefined` | signed overflow, misaligned access, invalid shifts, bad casts | ~1.2x | 51 | TSan | `-fsanitize=thread` | data races, lock-order inversion | ~5–15x | 52 | MSan | `-fsanitize=memory` | reads of uninitialised memory | ~3x | 53 54 MSan requires **every** linked library to be instrumented, including libc++. 55 Uninstrumented dependencies produce false positives. Skip it unless the whole 56 dependency tree can be rebuilt. 57 58 Useful runtime options: 59 60 ```bash 61 export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1:strict_string_checks=1 62 export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 63 export TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1 64 ``` 65 66 Other languages: 67 68 - **Rust**: `cargo +nightly miri test` for UB in `unsafe` blocks. Sanitizers via 69 `RUSTFLAGS="-Zsanitizer=address" cargo +nightly test`. 70 - **Go**: `go test -race ./...` — cheap, run it by default in CI. 71 72 TSan and the Go race detector only find races on code paths that actually 73 execute concurrently during the run. A clean run is weak evidence. Run the 74 concurrent tests repeatedly (`go test -race -count=50 -run TestConcurrent`). 75 76 ## Fuzzing 77 78 Fuzz any function that takes bytes, a string, or a file from outside the 79 process. Target the parse/decode boundary, not the whole application. 80 81 ### libFuzzer (C/C++) 82 83 ```c 84 // fuzz/fuzz_parse.c 85 #include <stdint.h> 86 #include <stddef.h> 87 #include "parser.h" 88 89 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { 90 if (size > 64 * 1024) return -1; // reject oversized inputs 91 parser_ctx *ctx = parser_new(); 92 parser_feed(ctx, data, size); // must not crash on any input 93 parser_free(ctx); // ASan catches leaks here 94 return 0; 95 } 96 ``` 97 98 ```bash 99 clang -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all \ 100 -g -O1 fuzz/fuzz_parse.c parser.c -o fuzz_parse 101 102 mkdir -p corpus 103 ./fuzz_parse corpus/ -max_total_time=120 -print_final_stats=1 104 ``` 105 106 Useful flags: `-jobs=$(nproc)` for parallel runs, `-max_len=` to bound input 107 size, `-dict=file.dict` to supply format keywords, `-runs=N` for a bounded 108 deterministic run in CI. 109 110 ### cargo-fuzz (Rust) 111 112 ```bash 113 cargo fuzz init 114 cargo fuzz add parse_target 115 cargo fuzz run parse_target -- -max_total_time=120 116 ``` 117 118 ```rust 119 // fuzz_targets/parse_target.rs 120 #![no_main] 121 use libfuzzer_sys::fuzz_target; 122 123 fuzz_target!(|data: &[u8]| { 124 let _ = my_crate::parse(data); // must not panic 125 }); 126 ``` 127 128 For structured input, use the `arbitrary` crate: 129 `fuzz_target!(|input: MyStruct| { ... })`. 130 131 ### Go native fuzzing 132 133 ```go 134 func FuzzParse(f *testing.F) { 135 f.Add([]byte(`{"a":1}`)) // seed corpus 136 f.Fuzz(func(t *testing.T, data []byte) { 137 v, err := Parse(data) 138 if err != nil { 139 return // errors are fine; panics are not 140 } 141 out, err := Marshal(v) // round-trip property 142 if err != nil { 143 t.Fatalf("marshal failed on parsed input: %v", err) 144 } 145 if v2, _ := Parse(out); !reflect.DeepEqual(v, v2) { 146 t.Fatalf("round trip mismatch") 147 } 148 }) 149 } 150 ``` 151 152 ```bash 153 go test -fuzz=FuzzParse -fuzztime=120s ./pkg/parser 154 ``` 155 156 Failing inputs are written to `testdata/fuzz/FuzzParse/` and **become 157 permanent regression tests** on subsequent `go test` runs. Commit them. 158 159 ### Seed corpus 160 161 Fuzzing without seeds wastes most of its budget rediscovering valid syntax. 162 Seed from existing test fixtures: 163 164 ```bash 165 mkdir -p corpus && cp tests/fixtures/*.json corpus/ 166 ``` 167 168 Minimise the corpus periodically: `./fuzz_target -merge=1 corpus_min/ corpus/`. 169 170 ## Triage of a crash 171 172 1. **Reproduce deterministically**: `./fuzz_parse crash-<sha1>` — libFuzzer 173 writes the failing input to a `crash-*` file in the working directory. 174 Go writes to `testdata/fuzz/`. 175 2. **Minimise**: `./fuzz_parse -minimize_crash=1 -runs=10000 crash-<sha1>`. 176 Go: `go test -run=FuzzParse/<hash>`. 177 3. **Commit the minimised input as a unit test before fixing.** It must fail 178 on unfixed code — show that run. 179 4. Read the sanitizer stack trace to identify the mechanism. ASan reports both 180 the faulting access and the allocation/free sites; all three matter. 181 5. Fix, then re-run the fuzzer for at least the original duration to confirm 182 the crash is gone and no new one surfaced. 183 184 ## CI integration 185 186 Fuzzing in CI is a bounded regression run, not a discovery campaign: 187 188 ```bash 189 ./fuzz_parse corpus/ -runs=100000 -max_total_time=60 190 go test -fuzz=FuzzParse -fuzztime=60s ./... 191 ``` 192 193 Long campaigns belong on a schedule (nightly, hours) or on OSS-Fuzz, which is 194 free for open-source projects and runs continuously. Always run the committed 195 corpus as plain tests on every PR — that is the regression suite.