Fuzz Testing

Why Fuzz Testing Matters - The Hidden Bug Hunter

Imagine you've built a perfect door lock that works flawlessly with every key you've ever tested. You've tried it with hundreds of different keys, and it always works perfectly. But one day, someone comes along with a bent paperclip, a hairpin, and a piece of bubble gum—and suddenly your "perfect" lock opens. That's the difference between unit testing and fuzz testing.

Real-World Disasters Found by Fuzzing:

  • CVE-2021-33195: Go's net.ParseIP vulnerability that caused buffer overflows with specially crafted IPv6 addresses. Found by fuzzing, affected millions of applications.
  • Heartbleed: The OpenSSL vulnerability that could have been discovered by fuzzing TLS handshake implementations. Cost: $500 million in global damages.
  • Cloudflare Outage: A regex DoS vulnerability took down major websites. Could have been prevented with proper fuzz testing.

The Hard Truth: Your code has bugs you don't know about. Unit tests verify your code works with inputs you expect. Fuzz tests discover what happens with inputs you never imagined.

Learning Objectives

By the end of this article, you will be able to:

  • Design effective fuzz tests that find bugs before they reach production
  • Write fuzzable code with clear invariants and properties
  • Use Go's native fuzzing to find security vulnerabilities and edge cases
  • Implement coverage-guided fuzzing for maximum bug discovery
  • Integrate fuzzing into CI/CD for continuous security testing

Core Concepts: Beyond Traditional Testing

Fuzz testing explores the vast space of unexpected inputs to find:

  • Panics and crashes that cause service outages
  • Security vulnerabilities like buffer overflows and injection attacks
  • Logic errors that violate fundamental assumptions
  • Performance issues through pathological inputs

Fuzzing vs Unit Testing:

 1// Unit Test: Verifies expected behavior
 2func TestReverseString(t *testing.T) {
 3    input := "hello"
 4    expected := "olleh"
 5
 6    result := ReverseString(input)
 7    if result != expected {
 8        t.Errorf("Expected %q, got %q", expected, result)
 9    }
10}
11
12// Fuzz Test: Discovers unexpected behavior
13func FuzzReverseString(f *testing.F) {
14    f.Add("hello")  // Seed input
15    f.Add("")       // Edge case
16    f.Add("a")      // Single character
17
18    f.Fuzz(func(t *testing.T, input string) {
19        // Property: Reversing twice should return original
20        reversed := ReverseString(input)
21        doubleReversed := ReverseString(reversed)
22
23        if input != doubleReversed {
24            t.Errorf("Property violated: %q != %q", input, doubleReversed)
25        }
26
27        // Property: Should never panic
28        // Fuzzer will automatically catch panics as failures
29    })
30}

Key Insight: Unit tests ask "Does it work correctly?" Fuzz tests ask "Can I break it?"

Understanding Coverage-Guided Fuzzing

Coverage-guided fuzzing is the secret weapon that makes modern fuzzing effective. Instead of generating completely random inputs, the fuzzer monitors which code paths get executed and intelligently mutates inputs to explore new paths.

How Coverage Guidance Works

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7// Example function with multiple code paths
 8func ClassifyInput(input string) string {
 9    // Path 1: Empty string
10    if len(input) == 0 {
11        return "empty"
12    }
13
14    // Path 2: Short string
15    if len(input) < 5 {
16        return "short"
17    }
18
19    // Path 3: Contains "special"
20    if contains(input, "special") {
21        return "special"
22    }
23
24    // Path 4: Long string
25    if len(input) > 100 {
26        return "long"
27    }
28
29    // Path 5: Default
30    return "normal"
31}
32
33func contains(s, substr string) bool {
34    for i := 0; i <= len(s)-len(substr); i++ {
35        if s[i:i+len(substr)] == substr {
36            return true
37        }
38    }
39    return false
40}
41
42// Coverage-guided fuzzing will automatically:
43// 1. Start with seed inputs
44// 2. Monitor which paths are executed
45// 3. Mutate inputs to explore unvisited paths
46// 4. Focus on inputs that increase coverage
47
48func FuzzClassifyInput(f *testing.F) {
49    // Seed corpus - helps fuzzer start
50    f.Add("")              // Triggers path 1
51    f.Add("hi")            // Triggers path 2
52    f.Add("hello")         // Triggers path 5
53    f.Add("special case")  // Triggers path 3
54    // Note: Fuzzer will discover path 4 (long) automatically
55
56    f.Fuzz(func(t *testing.T, input string) {
57        // Just call the function - fuzzer tracks coverage
58        result := ClassifyInput(input)
59
60        // Verify properties
61        if result == "empty" && len(input) != 0 {
62            t.Errorf("Classified non-empty as empty")
63        }
64
65        if result == "short" && len(input) >= 5 {
66            t.Errorf("Classified long as short")
67        }
68
69        if result == "special" && !contains(input, "special") {
70            t.Errorf("Classified as special without keyword")
71        }
72    })
73}
74
75func main() {
76    testCases := []string{
77        "",
78        "hi",
79        "hello world",
80        "special case",
81        string(make([]byte, 150)), // Long string
82    }
83
84    fmt.Println("Classification Results:")
85    for _, tc := range testCases {
86        display := tc
87        if len(display) > 30 {
88            display = display[:30] + "..."
89        }
90        result := ClassifyInput(tc)
91        fmt.Printf("Input %-35s -> %s\n", fmt.Sprintf("%q", display), result)
92    }
93}

Mutation Strategies

The fuzzer uses sophisticated mutation strategies to generate interesting inputs:

 1package main
 2
 3import (
 4    "fmt"
 5    "testing"
 6)
 7
 8// Mutation strategies the fuzzer employs:
 9// 1. Bit flipping - flip individual bits
10// 2. Byte insertion - add random bytes
11// 3. Byte deletion - remove bytes
12// 4. Byte substitution - replace bytes
13// 5. Chunk operations - swap, duplicate chunks
14// 6. Dictionary-based mutations - use known tokens
15
16type MutationExample struct {
17    original string
18}
19
20func Demonstrate() {
21    fmt.Println("Original input:", m.original)
22    fmt.Println("\nPossible mutations:")
23
24    // Bit flip
25    fmt.Println("1. Bit flip:      ", m.bitFlip())
26
27    // Byte insertion
28    fmt.Println("2. Insert byte:   ", m.insertByte())
29
30    // Byte deletion
31    fmt.Println("3. Delete byte:   ", m.deleteByte())
32
33    // Byte substitution
34    fmt.Println("4. Substitute:    ", m.substitute())
35
36    // Chunk swap
37    fmt.Println("5. Chunk swap:    ", m.chunkSwap())
38}
39
40func bitFlip() string {
41    if len(m.original) == 0 {
42        return m.original
43    }
44    bytes := []byte(m.original)
45    bytes[0] ^= 1 // Flip least significant bit
46    return string(bytes)
47}
48
49func insertByte() string {
50    bytes := []byte(m.original)
51    if len(bytes) == 0 {
52        return "X" + m.original
53    }
54    return string(bytes[:1]) + "X" + string(bytes[1:])
55}
56
57func deleteByte() string {
58    if len(m.original) <= 1 {
59        return ""
60    }
61    return m.original[1:]
62}
63
64func substitute() string {
65    if len(m.original) == 0 {
66        return m.original
67    }
68    bytes := []byte(m.original)
69    bytes[0] = 'Z'
70    return string(bytes)
71}
72
73func chunkSwap() string {
74    if len(m.original) < 4 {
75        return m.original
76    }
77    mid := len(m.original) / 2
78    return m.original[mid:] + m.original[:mid]
79}
80
81func main() {
82    example := &MutationExample{original: "hello"}
83    example.Demonstrate()
84
85    fmt.Println("\n--- Coverage-Guided Process ---")
86    fmt.Println("1. Fuzzer starts with 'hello'")
87    fmt.Println("2. Tries 'iello' (bit flip) - new coverage? Keep it")
88    fmt.Println("3. Tries 'Xhello' (insert) - new coverage? Keep it")
89    fmt.Println("4. Repeats with promising mutations")
90    fmt.Println("5. Builds corpus of interesting inputs")
91}

Practical Examples - From Basic to Advanced

Let's start with a simple string reversal function and progressively discover issues through fuzzing.

Example 1: String Reversal - The First Bug

 1package main
 2
 3import (
 4    "fmt"
 5    "testing"
 6)
 7
 8// StringReverser demonstrates common pitfalls in string processing
 9type StringReverser struct{}
10
11func Reverse(input string) string {
12    // Convert to runes
13    runes := []rune(input)
14
15    // Reverse in place
16    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
17        runes[i], runes[j] = runes[j], runes[i]
18    }
19
20    return string(runes)
21}
22
23func ReverseBuggy(input string) string {
24    // BUG: Treats string as bytes
25    bytes := []byte(input)
26
27    for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
28        bytes[i], bytes[j] = bytes[j], bytes[i]
29    }
30
31    return string(bytes)  // Corrupts multi-byte Unicode characters
32}
33
34func FuzzStringReverser(f *testing.F) {
35    // Seed corpus: Help fuzzer find interesting cases
36    f.Add("hello")                    // ASCII
37    f.Add("world")                    // ASCII
38    f.Add("")                         // Empty string
39    f.Add("a")                        // Single character
40    f.Add("hello, 世界")              // Mixed ASCII and Unicode
41    f.Add("😀👍")                     // Emoji
42
43    f.Fuzz(func(t *testing.T, input string) {
44        reverser := &StringReverser{}
45
46        // Test both versions
47        correctResult := reverser.Reverse(input)
48        buggyResult := reverser.ReverseBuggy(input)
49
50        // Property 1: Double reverse should return original
51        doubleCorrect := reverser.Reverse(correctResult)
52        if doubleCorrect != input {
53            t.Errorf("Correct version failed double reverse: %q -> %q -> %q",
54                input, correctResult, doubleCorrect)
55        }
56
57        // Property 2: Results should match for ASCII, differ for Unicode
58        isASCII := func(s string) bool {
59            for _, r := range s {
60                if r > 127 {
61                    return false
62                }
63            }
64            return true
65        }
66
67        if isASCII(input) && correctResult != buggyResult {
68            t.Errorf("Results differ for ASCII input %q: correct=%q, buggy=%q",
69                input, correctResult, buggyResult)
70        }
71
72        // Property 3: Length should be preserved
73        if len(correctResult) != len(input) {
74            t.Errorf("Length changed: %d -> %d", len(input), len(correctResult))
75        }
76    })
77}
78
79func main() {
80    reverser := &StringReverser{}
81
82    // Demonstrate the bug
83    testInputs := []string{
84        "hello",           // ASCII - works in both
85        "世界",             // Unicode - broken in buggy version
86        "café",            // Accented characters
87        "👍 hello",       // Mixed emoji and text
88    }
89
90    for _, input := range testInputs {
91        correct := reverser.Reverse(input)
92        buggy := reverser.ReverseBuggy(input)
93
94        fmt.Printf("Input: %-15q | Correct: %-15q | Buggy: %-15q\n",
95            input, correct, buggy)
96    }
97}

Fuzzer Output:

fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 84723, new interesting: 45
fuzz: elapsed: 6s, execs: 169421, new interesting: 67
--- FAIL: FuzzStringReverser failed after 8s
    testing.go:1350: panic: runtime error: slice bounds out of range [:18446744071582087930]
    goroutine 6 [running]:
    ...

Key Finding: The fuzzer quickly discovered that the buggy version corrupts Unicode strings, while the correct version handles them properly.

Example 2: JSON Parser - Finding Security Vulnerabilities

  1package main
  2
  3import (
  4    "bytes"
  5    "encoding/json"
  6    "fmt"
  7    "testing"
  8)
  9
 10// SafeJSONParser provides secure JSON parsing with validation
 11type SafeJSONParser struct {
 12    maxDepth    int
 13    maxElements int
 14}
 15
 16func NewSafeJSONParser() *SafeJSONParser {
 17    return &SafeJSONParser{
 18        maxDepth:    100,  // Prevent stack overflow
 19        maxElements: 1000, // Prevent memory exhaustion
 20    }
 21}
 22
 23// Parse safely unmarshals JSON with security checks
 24func (p *SafeJSONParser) Parse(data []byte) (interface{}, error) {
 25    // Security check 1: Input size limit
 26    if len(data) > 1024*1024 { // 1MB limit
 27        return nil, fmt.Errorf("JSON too large: %d bytes", len(data))
 28    }
 29
 30    // Security check 2: Basic structure validation
 31    if p.hasMaliciousPatterns(data) {
 32        return nil, fmt.Errorf("potentially malicious JSON detected")
 33    }
 34
 35    var result interface{}
 36    decoder := json.NewDecoder(bytes.NewReader(data))
 37
 38    // Security check 3: Prevent deep nesting during parse
 39    decoder.DisallowUnknownFields()
 40
 41    err := decoder.Decode(&result)
 42    if err != nil {
 43        return nil, fmt.Errorf("JSON parse error: %w", err)
 44    }
 45
 46    // Security check 4: Validate parsed structure
 47    if err := p.validateStructure(result, 0); err != nil {
 48        return nil, fmt.Errorf("unsafe JSON structure: %w", err)
 49    }
 50
 51    return result, nil
 52}
 53
 54// hasMaliciousPatterns checks for known attack patterns
 55func (p *SafeJSONParser) hasMaliciousPatterns(data []byte) bool {
 56    patterns := [][]byte{
 57        []byte(`{{`),           // Template injection
 58        []byte(`${`),            // Another template syntax
 59        []byte("<script"),       // XSS attempt
 60        []byte("javascript:"),   // Protocol injection
 61    }
 62
 63    for _, pattern := range patterns {
 64        if bytes.Contains(data, pattern) {
 65            return true
 66        }
 67    }
 68
 69    return false
 70}
 71
 72// validateStructure checks for dangerous structures
 73func (p *SafeJSONParser) validateStructure(value interface{}, depth int) error {
 74    if depth > p.maxDepth {
 75        return fmt.Errorf("JSON too deeply nested: %d", depth)
 76    }
 77
 78    switch v := value.(type) {
 79    case map[string]interface{}:
 80        if len(v) > p.maxElements {
 81            return fmt.Errorf("too many object elements: %d", len(v))
 82        }
 83        for _, val := range v {
 84            if err := p.validateStructure(val, depth+1); err != nil {
 85                return err
 86            }
 87        }
 88
 89    case []interface{}:
 90        if len(v) > p.maxElements {
 91            return fmt.Errorf("too many array elements: %d", len(v))
 92        }
 93        for _, val := range v {
 94            if err := p.validateStructure(val, depth+1); err != nil {
 95                return err
 96            }
 97        }
 98
 99    case string:
100        if len(v) > p.maxElements {
101            return fmt.Errorf("string too long: %d characters", len(v))
102        }
103    }
104
105    return nil
106}
107
108// VulnerableJSONParser represents a vulnerable parser for comparison
109type VulnerableJSONParser struct{}
110
111func (v *VulnerableJSONParser) Parse(data []byte) (interface{}, error) {
112    // Vulnerable: No security checks
113    var result interface{}
114    err := json.Unmarshal(data, &result)
115    return result, err
116}
117
118func FuzzJSONParsers(f *testing.F) {
119    // Seed corpus with various JSON patterns
120    f.Add([]byte(`{"name":"Alice","age":30}`))
121    f.Add([]byte(`{"nested":{"deep":{"structure":{}}}}`))
122    f.Add([]byte(`{"array":[1,2,3,4,5]}`))
123    f.Add([]byte(`"simple string"`))
124    f.Add([]byte(`null`))
125    f.Add([]byte(`[]`))
126    f.Add([]byte(`{}`))
127
128    // Add potentially dangerous inputs
129    f.Add([]byte(`{"template":"{{.user}}"}`))
130    f.Add([]byte(`{"script":"<script>alert('xss')</script>"}`))
131
132    f.Fuzz(func(t *testing.T, data []byte) {
133        safeParser := NewSafeJSONParser()
134        vulnerableParser := &VulnerableJSONParser{}
135
136        // Test safe parser
137        safeResult, safeErr := safeParser.Parse(data)
138
139        // Test vulnerable parser
140        vulnerableResult, vulnerableErr := vulnerableParser.Parse(data)
141
142        // If both succeed, results should match for valid JSON
143        if safeErr == nil && vulnerableErr == nil {
144            // Round-trip test for safe parser
145            remarshaled, err := json.Marshal(safeResult)
146            if err != nil {
147                t.Errorf("Safe parser result cannot be remarshaled: %v", err)
148            }
149
150            // Re-parse should work
151            _, err = safeParser.Parse(remarshaled)
152            if err != nil {
153                t.Errorf("Round-trip failed: %v", err)
154            }
155        }
156
157        // Safe parser should reject malicious inputs that vulnerable parser accepts
158        if safeErr != nil && vulnerableErr == nil {
159            t.Logf("Safe parser correctly rejected potentially malicious JSON: %v", safeErr)
160        }
161
162        // Properties to verify when parsing succeeds:
163        if safeErr == nil {
164            // 1. Result should not be nil for non-null JSON
165            if safeResult == nil && !bytes.Equal(data, []byte("null")) {
166                t.Error("Parser returned nil for non-null JSON")
167            }
168
169            // 2. Should be able to marshal the result
170            _, err := json.Marshal(safeResult)
171            if err != nil {
172                t.Errorf("Cannot marshal parsing result: %v", err)
173            }
174        }
175    })
176}
177
178func main() {
179    parser := NewSafeJSONParser()
180
181    testCases := []struct {
182        name       string
183        data       []byte
184        shouldPass bool
185    }{
186        {"Valid JSON", []byte(`{"name":"Alice","age":30}`), true},
187        {"Deep nesting", []byte(`{"a":` + repeat(`{"b":`, 200) + `"x"` + repeat(`}`, 200) + `}`), false},
188        {"Large array", []byte(`[` + repeat(`"x",`, 2000) + `"x"]`), false},
189        {"Malicious template", []byte(`{"template":"{{.user}}"}`), false},
190        {"XSS attempt", []byte(`{"script":"<script>alert(1)</script>"}`), false},
191    }
192
193    for _, tc := range testCases {
194        result, err := parser.Parse(tc.data)
195        passed := err == nil
196        status := "✅"
197        if passed != tc.shouldPass {
198            status = "❌"
199        }
200
201        fmt.Printf("%s %-20s: %v\n", status, tc.name, passed)
202        if passed {
203            fmt.Printf("  Result: %+v\n", result)
204        } else {
205            fmt.Printf("  Error: %v\n", err)
206        }
207    }
208}
209
210func repeat(s string, count int) string {
211    result := make([]byte, 0, len(s)*count)
212    for i := 0; i < count; i++ {
213        result = append(result, s...)
214    }
215    return string(result)
216}

Fuzzer Discoveries:

fuzz: elapsed: 5s, execs: 127431, new interesting: 89
fuzz: elapsed: 10s, execs: 254892, new interesting: 124
--- FAIL: FuzzJSONParsers failed after 12s
    testing.go:1350: memory allocation size 1073741824 exceeds limit
    goroutine 7 [running]:
    ...

Example 3: Rate Limiter - Finding Race Conditions

  1package main
  2
  3import (
  4    "fmt"
  5    "sync"
  6    "sync/atomic"
  7    "testing"
  8    "time"
  9)
 10
 11// RateLimiter implements token bucket with thread safety
 12type RateLimiter struct {
 13    capacity   int64
 14    tokens     int64  // Atomic
 15    refillRate int64  // tokens per second
 16    lastRefill int64  // Unix nanoseconds, atomic
 17    mu         sync.Mutex
 18}
 19
 20func NewRateLimiter(capacity, refillRate int64) *RateLimiter {
 21    now := time.Now().UnixNano()
 22    return &RateLimiter{
 23        capacity:   capacity,
 24        tokens:     capacity,
 25        refillRate: refillRate,
 26        lastRefill: now,
 27    }
 28}
 29
 30// Allow checks if a request should be allowed
 31func (rl *RateLimiter) Allow() bool {
 32    // Refill tokens based on elapsed time
 33    rl.refill()
 34
 35    // Try to consume a token atomically
 36    for {
 37        current := atomic.LoadInt64(&rl.tokens)
 38        if current <= 0 {
 39            return false
 40        }
 41
 42        // Compare and swap
 43        if atomic.CompareAndSwapInt64(&rl.tokens, current, current-1) {
 44            return true
 45        }
 46        // Retry if CAS failed
 47    }
 48}
 49
 50// refill updates the token count based on elapsed time
 51func (rl *RateLimiter) refill() {
 52    now := time.Now().UnixNano()
 53    last := atomic.LoadInt64(&rl.lastRefill)
 54
 55    // Try to update lastRefill time
 56    if atomic.CompareAndSwapInt64(&rl.lastRefill, last, now) {
 57        // We won the race, calculate tokens to add
 58        elapsed := now - last
 59        tokensToAdd := (elapsed * rl.refillRate) / int64(time.Second)
 60
 61        if tokensToAdd > 0 {
 62            for {
 63                current := atomic.LoadInt64(&rl.tokens)
 64                newTokens := current + tokensToAdd
 65
 66                // Don't exceed capacity
 67                if newTokens > rl.capacity {
 68                    newTokens = rl.capacity
 69                }
 70
 71                if atomic.CompareAndSwapInt64(&rl.tokens, current, newTokens) {
 72                    break
 73                }
 74                // Retry if CAS failed
 75            }
 76        }
 77    }
 78}
 79
 80// GetTokens returns current token count
 81func (rl *RateLimiter) GetTokens() int64 {
 82    rl.refill()
 83    return atomic.LoadInt64(&rl.tokens)
 84}
 85
 86func FuzzRateLimiter(f *testing.F) {
 87    // Seed with common rate limiter scenarios
 88    f.Add(int64(10), int64(5), int64(100))   // capacity, rate, requests
 89    f.Add(int64(100), int64(10), int64(1000))
 90    f.Add(int64(1), int64(1), int64(50))
 91
 92    f.Fuzz(func(t *testing.T, capacity, rate, numRequests int64) {
 93        // Bounds checking
 94        if capacity <= 0 || capacity > 1000 {
 95            return
 96        }
 97        if rate <= 0 || rate > 1000 {
 98            return
 99        }
100        if numRequests <= 0 || numRequests > 1000 {
101            return
102        }
103
104        rl := NewRateLimiter(capacity, rate)
105
106        var allowed, denied int64
107
108        // Test concurrent access
109        var wg sync.WaitGroup
110        numWorkers := int64(10)
111        requestsPerWorker := numRequests / numWorkers
112
113        for i := int64(0); i < numWorkers; i++ {
114            wg.Add(1)
115            go func(workerID int64) {
116                defer wg.Done()
117
118                for j := int64(0); j < requestsPerWorker; j++ {
119                    if rl.Allow() {
120                        atomic.AddInt64(&allowed, 1)
121                    } else {
122                        atomic.AddInt64(&denied, 1)
123                    }
124
125                    // Small delay to allow refilling
126                    time.Sleep(time.Microsecond)
127                }
128            }(i)
129        }
130
131        wg.Wait()
132
133        // Verify invariants
134        totalRequests := allowed + denied
135        if totalRequests != numRequests {
136            t.Errorf("Request count mismatch: %d + %d != %d",
137                allowed, denied, numRequests)
138        }
139
140        // Tokens should never be negative
141        tokens := rl.GetTokens()
142        if tokens < 0 {
143            t.Errorf("Negative tokens: %d", tokens)
144        }
145
146        // Tokens should never exceed capacity
147        if tokens > capacity {
148            t.Errorf("Tokens exceed capacity: %d > %d", tokens, capacity)
149        }
150
151        // Should not allow more requests than capacity initially
152        if allowed > capacity && numRequests >= capacity {
153            t.Errorf("Allowed more than capacity: %d > %d", allowed, capacity)
154        }
155    })
156}
157
158func main() {
159    // Demonstrate rate limiter behavior
160    rl := NewRateLimiter(5, 2) // 5 tokens, 2 tokens/second
161
162    fmt.Println("Rate Limiter Demo:")
163
164    for i := 0; i < 15; i++ {
165        allowed := rl.Allow()
166        tokens := rl.GetTokens()
167
168        status := "❌ DENIED"
169        if allowed {
170            status = "✅ ALLOWED"
171        }
172
173        fmt.Printf("Request %2d: %-10s | Tokens: %d\n", i+1, status, tokens)
174
175        if i%5 == 4 {
176            fmt.Println("  [Waiting 1 second for refill...]")
177            time.Sleep(1 * time.Second)
178        }
179    }
180}

Corpus Management and Optimization

Effective corpus management is crucial for finding bugs efficiently. The corpus is the collection of inputs that the fuzzer uses as a starting point.

Building an Effective Seed Corpus

  1package main
  2
  3import (
  4    "encoding/json"
  5    "fmt"
  6    "os"
  7    "path/filepath"
  8    "testing"
  9)
 10
 11// CorpusManager handles test corpus for fuzzing
 12type CorpusManager struct {
 13    corpusDir string
 14}
 15
 16func NewCorpusManager(dir string) *CorpusManager {
 17    return &CorpusManager{corpusDir: dir}
 18}
 19
 20// SaveSeed saves a seed input to corpus
 21func (cm *CorpusManager) SaveSeed(name string, data []byte) error {
 22    if err := os.MkdirAll(cm.corpusDir, 0755); err != nil {
 23        return fmt.Errorf("create corpus dir: %w", err)
 24    }
 25
 26    path := filepath.Join(cm.corpusDir, name)
 27    if err := os.WriteFile(path, data, 0644); err != nil {
 28        return fmt.Errorf("write seed: %w", err)
 29    }
 30
 31    return nil
 32}
 33
 34// LoadSeeds loads all seeds from corpus directory
 35func (cm *CorpusManager) LoadSeeds() ([][]byte, error) {
 36    entries, err := os.ReadDir(cm.corpusDir)
 37    if err != nil {
 38        return nil, fmt.Errorf("read corpus dir: %w", err)
 39    }
 40
 41    var seeds [][]byte
 42    for _, entry := range entries {
 43        if entry.IsDir() {
 44            continue
 45        }
 46
 47        path := filepath.Join(cm.corpusDir, entry.Name())
 48        data, err := os.ReadFile(path)
 49        if err != nil {
 50            continue
 51        }
 52
 53        seeds = append(seeds, data)
 54    }
 55
 56    return seeds, nil
 57}
 58
 59// Example: Building corpus for URL parser
 60func BuildURLCorpus() []string {
 61    return []string{
 62        // Valid URLs
 63        "http://example.com",
 64        "https://example.com",
 65        "https://example.com:8080",
 66        "https://user:pass@example.com",
 67        "https://example.com/path",
 68        "https://example.com/path?query=value",
 69        "https://example.com/path?q1=v1&q2=v2",
 70        "https://example.com/path#fragment",
 71        "https://example.com:8080/path?query=value#fragment",
 72
 73        // Edge cases
 74        "",                                    // Empty
 75        "://no-scheme.com",                    // Missing scheme
 76        "http://",                             // No host
 77        "http://example.com:abc",              // Invalid port
 78        "http://example.com:-1",               // Negative port
 79        "http://example.com:999999",           // Port too large
 80        "http://[invalid-ipv6]",               // Malformed IPv6
 81        "http://256.256.256.256",              // Invalid IPv4
 82        "http://example.com//double/slash",    // Double slashes
 83        "http://example.com/path/../../../etc/passwd", // Path traversal
 84
 85        // Special characters
 86        "http://example.com/path with spaces",
 87        "http://example.com/path%20encoded",
 88        "http://example.com/unicode/世界",
 89        "http://example.com/emoji/👍",
 90        "http://example.com/path?query=<script>",
 91
 92        // International domain names
 93        "http://münchen.de",
 94        "http://中国.cn",
 95
 96        // Long inputs
 97        "http://example.com/" + string(make([]byte, 1000)),
 98        "http://" + string(make([]byte, 1000)) + ".com",
 99    }
100}
101
102// Example: Building corpus for JSON parser
103func BuildJSONCorpus() []string {
104    return []string{
105        // Valid JSON
106        `{}`,
107        `[]`,
108        `null`,
109        `true`,
110        `false`,
111        `123`,
112        `"string"`,
113        `{"key":"value"}`,
114        `{"nested":{"object":true}}`,
115        `[1,2,3]`,
116        `[[[]]]`,
117
118        // Edge cases
119        ``,                              // Empty
120        `{`,                             // Incomplete
121        `}`,                             // Just closing
122        `{{}`,                           // Unbalanced
123        `{"key":}`,                      // Missing value
124        `{"key":"value",}`,              // Trailing comma
125        `{key:"value"}`,                 // Unquoted key
126
127        // Deep nesting
128        repeat(`[`, 100) + repeat(`]`, 100),
129        repeat(`{"a":`, 100) + `null` + repeat(`}`, 100),
130
131        // Large values
132        `{"large":"` + string(make([]byte, 10000)) + `"}`,
133        `[` + repeat(`1,`, 1000) + `1]`,
134
135        // Special characters
136        `{"emoji":"👍"}`,
137        `{"unicode":"世界"}`,
138        `{"escaped":"\"quotes\""}`,
139        `{"newline":"line1\nline2"}`,
140    }
141}
142
143func repeat(s string, n int) string {
144    result := ""
145    for i := 0; i < n; i++ {
146        result += s
147    }
148    return result
149}
150
151// FuzzWithManagedCorpus demonstrates corpus management
152func FuzzWithManagedCorpus(f *testing.F) {
153    // Load existing corpus
154    manager := NewCorpusManager("testdata/fuzz/corpus")
155    seeds, err := manager.LoadSeeds()
156    if err == nil {
157        for _, seed := range seeds {
158            f.Add(seed)
159        }
160    }
161
162    // Add programmatic seeds
163    for _, urlStr := range BuildURLCorpus() {
164        f.Add([]byte(urlStr))
165    }
166
167    f.Fuzz(func(t *testing.T, data []byte) {
168        // Your fuzzing logic here
169        _ = data
170    })
171}
172
173func main() {
174    fmt.Println("=== URL Corpus Examples ===")
175    urlCorpus := BuildURLCorpus()
176    for i, url := range urlCorpus[:10] { // Show first 10
177        fmt.Printf("%2d. %s\n", i+1, url)
178    }
179
180    fmt.Println("\n=== JSON Corpus Examples ===")
181    jsonCorpus := BuildJSONCorpus()
182    for i, jsonStr := range jsonCorpus[:10] { // Show first 10
183        fmt.Printf("%2d. %s\n", i+1, jsonStr)
184    }
185
186    fmt.Printf("\nTotal URL corpus size: %d\n", len(urlCorpus))
187    fmt.Printf("Total JSON corpus size: %d\n", len(jsonCorpus))
188}

Common Patterns and Pitfalls

Pattern 1: Property-Based Testing

The most effective fuzz tests verify properties that should always be true:

 1// Good: Test invariants
 2func FuzzStringOperations(f *testing.F) {
 3    f.Fuzz(func(t *testing.T, a, b string) {
 4        // Property: Contains should be consistent with Index
 5        if strings.Contains(a, b) {
 6            if strings.Index(a, b) == -1 {
 7                t.Errorf("Contains true but Index returns -1")
 8            }
 9        }
10
11        // Property: String length preserved by double reverse
12        doubleReversed := reverse(reverse(a))
13        if len(doubleReversed) != len(a) {
14            t.Errorf("Length changed by double reverse")
15        }
16
17        // Property: Join should increase length by at least separators
18        joined := strings.Join([]string{a, b}, ",")
19        if len(joined) < len(a)+len(b) {
20            t.Errorf("Join result too short")
21        }
22    })
23}
24
25// Bad: Test specific outcomes
26func FuzzBadExample(f *testing.F) {
27    f.Fuzz(func(t *testing.T, input string) {
28        // This is just unit testing with random inputs
29        result := process(input)
30        if result != "expected" {  // What's "expected" for random input?
31            t.Errorf("Wrong result")
32        }
33    })
34}

Pattern 2: Progressive Complexity

Start simple, then add complexity:

 1// Level 1: Basic functionality
 2func FuzzBasic(f *testing.F) {
 3    f.Fuzz(func(t *testing.T, input string) {
 4        // Should never panic
 5        _ = basicFunction(input)
 6    })
 7}
 8
 9// Level 2: Add invariants
10func FuzzWithInvariants(f *testing.F) {
11    f.Fuzz(func(t *testing.T, input string) {
12        result := basicFunction(input)
13
14        // Should satisfy basic properties
15        if len(result) > maxExpectedLength {
16            t.Errorf("Result too long")
17        }
18    })
19}
20
21// Level 3: Complex scenarios
22func FuzzComplex(f *testing.F) {
23    f.Fuzz(func(t *testing.T, input1, input2 string) {
24        // Test interaction between multiple inputs
25        result1 := basicFunction(input1)
26        result2 := basicFunction(input2)
27
28        combined := combine(result1, result2)
29        if violatesInvariant(combined) {
30            t.Errorf("Combined input breaks invariant")
31        }
32    })
33}

Common Pitfalls

  1. Testing Implementation Details:
1// Bad: Tests internal implementation
2if bytes.Contains(result, []byte("internal_variable")) {
3    t.Error("Contains internal variable name")
4}
5
6// Good: Tests external behavior
7if !isValidOutput(result) {
8    t.Error("Invalid output format")
9}
  1. Assuming Input Distribution:
1// Bad: Assumes ASCII input
2func isSpecial(char byte) bool {
3    return char < 32  // Fails with UTF-8
4}
5
6// Good: Handles all inputs
7func isSpecial(r rune) bool {
8    return r < 32 || r > 126
9}
  1. Infinite Loops on Bad Input:
1// Bad: Can loop forever on malformed input
2for !isValid(input) {
3    input = transform(input)  // What if transform never makes it valid?
4}
5
6// Good: Limits iterations
7for i := 0; i < 1000 && !isValid(input); i++ {
8    input = transform(input)
9}

Integration and Mastery - Production Fuzzing

Setting Up Continuous Fuzzing

 1# .github/workflows/fuzz.yml
 2name: Continuous Fuzzing
 3
 4on:
 5  push:
 6    branches: [main]
 7  pull_request:
 8  schedule:
 9    - cron: '0 2 * * *'  # Daily at 2 AM
10
11jobs:
12  fuzz:
13    runs-on: ubuntu-latest
14    strategy:
15      matrix:
16        target:
17          - FuzzStringReverser
18          - FuzzJSONParsers
19          - FuzzRateLimiter
20        time:
21          - 30s
22          - 2m
23          - 5m
24
25    steps:
26      - uses: actions/checkout@v3
27
28      - name: Set up Go
29        uses: actions/setup-go@v4
30        with:
31          go-version: '1.21'
32
33      - name: Cache go modules
34        uses: actions/cache@v3
35        with:
36          path: ~/go/pkg/mod
37          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
38
39      - name: Run fuzzing
40        run: |
41          set -eo pipefail
42
43          echo "Starting fuzz test: ${{ matrix.target }}"
44          echo "Duration: ${{ matrix.time }}"
45
46          # Run with race detector for concurrent code
47          go test -race \
48                  -fuzz=${{ matrix.target }} \
49                  -fuzztime=${{ matrix.time }} \
50                  -parallel=4 \
51                  ./...
52
53          echo "Fuzzing completed successfully"          
54
55      - name: Upload corpus
56        if: always()
57        uses: actions/upload-artifact@v3
58        with:
59          name: fuzz-corpus-${{ matrix.target }}
60          path: testdata/fuzz/${{ matrix.target }}/
61          retention-days: 30

Corpus Management Best Practices

 1// corpus_test.go
 2package mypackage
 3
 4import (
 5    "testing"
 6    "time"
 7)
 8
 9func FuzzMyFunction(f *testing.F) {
10    // 1. Seed with diverse, realistic inputs
11    f.Add([]byte("valid_json_string"))
12    f.Add([]byte(""))                           // Empty
13    f.Add([]byte("{"))                          // Malformed start
14    f.Add([]byte("}"))                          // Malformed end
15    f.Add(make([]byte, 10000))                 // Large input
16    f.Add([]byte{0x00, 0xFF, 0xFE})          // Binary data
17
18    // 2. Add real-world examples
19    for _, example := range realWorldExamples {
20        f.Add(example)
21    }
22
23    f.Fuzz(func(t *testing.T, data []byte) {
24        // Early bounds checking improves performance
25        if len(data) > maxInputSize {
26            return  // Skip oversized inputs
27        }
28
29        // Test the function
30        result := MyFunction(data)
31
32        // Verify properties
33        if result != nil {
34            // Basic validation
35            if len(result.Output) > maxOutputSize {
36                t.Errorf("Output too large: %d bytes", len(result.Output))
37            }
38
39            // Round-trip test if applicable
40            if result.CanRoundTrip() {
41                roundTrip := result.ReverseProcess()
42                if !roundTrip.Equals(data) {
43                    t.Errorf("Round-trip failed")
44                }
45            }
46        }
47    })
48}
49
50// Helper: Generate realistic test data
51func generateRealWorldExamples() [][]byte {
52    return [][]byte{
53        []byte(`{"name":"John","age":30}`),      // JSON object
54        []byte(`[1,2,3,4,5]`),             // JSON array
55        []byte("user@example.com"),             // Email format
56        []byte("HTTP/1.1 200 OK\r\n..."),      // HTTP response
57        []byte("<?xml version='1.0'?>..."),   // XML document
58    }
59}

Fuzzing Different Types of Code

1. Parsers and Data Processors

 1func FuzzCSVParser(f *testing.F) {
 2    f.Add("name,age,city\nJohn,30,NYC")
 3    f.Add("a,b,c\n1,2,3\n4,5,6")
 4    f.Add("")
 5
 6    f.Fuzz(func(t *testing.T, csvData string) {
 7        parser := NewCSVParser()
 8
 9        // Should never panic on any input
10        records, err := parser.Parse(csvData)
11
12        if err == nil {
13            // Verify invariants when parsing succeeds
14            if len(records) == 0 && csvData != "" {
15                t.Error("Empty result from non-empty CSV")
16            }
17
18            // All records should have same number of fields
19            if len(records) > 1 {
20                fieldCount := len(records[0])
21                for i, record := range records[1:] {
22                    if len(record) != fieldCount {
23                        t.Errorf("Record %d has %d fields, expected %d",
24                            i+1, len(record), fieldCount)
25                    }
26                }
27            }
28        }
29    })
30}

2. Network Protocol Handlers

 1func FuzzHTTPHandler(f *testing.F) {
 2    f.Add([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
 3    f.Add([]byte("POST /api HTTP/1.1\r\nContent-Length: 10\r\n\r\n1234567890"))
 4    f.Add([]byte("INVALID REQUEST"))
 5
 6    f.Fuzz(func(t *testing.T, requestData []byte) {
 7        // Create request from fuzzed data
 8        req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(requestData)))
 9        if err != nil {
10            // Malformed request is acceptable
11            return
12        }
13
14        // Test handler with request
15        rr := httptest.NewRecorder()
16        MyHTTPHandler(rr, req)
17
18        // Response should be valid HTTP
19        resp := rr.Result()
20        if resp.StatusCode < 100 || resp.StatusCode > 599 {
21            t.Errorf("Invalid status code: %d", resp.StatusCode)
22        }
23
24        // Should not panic or hang
25        if resp.Body == nil {
26            t.Error("Response body is nil")
27        }
28    })
29}

3. Database Operations

 1func FuzzSQLBuilder(f *testing.F) {
 2    f.Add("users", "name", "Alice")
 3    f.Add("orders", "id", "12345")
 4
 5    f.Fuzz(func(t *testing.T, table, column, value string) {
 6        // Validate inputs to prevent SQL injection in the fuzzer itself
 7        if !isValidIdentifier(table) || !isValidIdentifier(column) {
 8            return
 9        }
10
11        // Build query safely
12        query := SQLBuilder{}
13        query.Select(column).From(table).Where("name = ?", value)
14
15        sql, args, err := query.Build()
16        if err != nil {
17            return // Build errors are acceptable
18        }
19
20        // Generated SQL should be parameterized
21        if !bytes.Contains(sql, []byte("?")) {
22            t.Errorf("Query not parameterized: %s", sql)
23        }
24
25        // Args should match placeholder count
26        placeholderCount := bytes.Count(sql, []byte("?"))
27        if len(args) != placeholderCount {
28            t.Errorf("Args count mismatch: %d args, %d placeholders",
29                len(args), placeholderCount)
30        }
31    })
32}

Practice Exercises

Exercise 1: Template Engine Fuzzer

Build a simple template engine and write fuzz tests to find security vulnerabilities.

Requirements:

  1. Template syntax: Hello {{.name}}!
  2. Support nested objects: {{.user.name}}
  3. Handle missing fields gracefully
  4. Prevent infinite recursion
  5. Block template injection attacks
Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "regexp"
  6    "strings"
  7    "testing"
  8)
  9
 10// TemplateEngine processes simple template syntax
 11type TemplateEngine struct {
 12    maxRecursion int
 13}
 14
 15func NewTemplateEngine() *TemplateEngine {
 16    return &TemplateEngine{maxRecursion: 10}
 17}
 18
 19// Execute renders a template with given data
 20func (te *TemplateEngine) Execute(template string, data map[string]interface{}) (string, error) {
 21    return te.executeWithDepth(template, data, 0)
 22}
 23
 24func (te *TemplateEngine) executeWithDepth(template string, data map[string]interface{}, depth int) (string, error) {
 25    if depth > te.maxRecursion {
 26        return "", fmt.Errorf("template recursion too deep")
 27    }
 28
 29    // Security check: Template size limit
 30    if len(template) > 10000 {
 31        return "", fmt.Errorf("template too large")
 32    }
 33
 34    // Find all {{.field}} patterns
 35    re := regexp.MustCompile(`\{\{\.([^}]+)\}\}`)
 36    result := template
 37
 38    for {
 39        matches := re.FindStringSubmatchIndex(result)
 40        if len(matches) == 0 {
 41            break
 42        }
 43
 44        fullMatch := result[matches[0]:matches[1]]
 45        fieldPath := result[matches[2]:matches[3]]
 46
 47        // Security check: Validate field path
 48        if err := te.validateFieldPath(fieldPath); err != nil {
 49            return "", fmt.Errorf("invalid field path %q: %w", fieldPath, err)
 50        }
 51
 52        value := te.getValue(data, fieldPath)
 53        valueStr := te.interfaceToString(value)
 54
 55        // Recursive expansion for nested templates
 56        expanded, err := te.executeWithDepth(valueStr, data, depth+1)
 57        if err != nil {
 58            return "", err
 59        }
 60
 61        result = result[:matches[0]] + expanded + result[matches[1]:]
 62    }
 63
 64    return result, nil
 65}
 66
 67// validateFieldPath checks for dangerous field paths
 68func (te *TemplateEngine) validateFieldPath(path string) error {
 69    // Security: Check for injection patterns
 70    dangerousPatterns := []string{
 71        "{{", "}}", "<script", "javascript:",
 72        "data:", "vbscript:", "onload=", "onerror=",
 73    }
 74
 75    lowerPath := strings.ToLower(path)
 76    for _, pattern := range dangerousPatterns {
 77        if strings.Contains(lowerPath, pattern) {
 78            return fmt.Errorf("potentially dangerous pattern: %s", pattern)
 79        }
 80    }
 81
 82    // Security: Check path length
 83    if len(path) > 100 {
 84        return fmt.Errorf("field path too long")
 85    }
 86
 87    return nil
 88}
 89
 90// getValue retrieves value from data using dot notation
 91func (te *TemplateEngine) getValue(data map[string]interface{}, path string) interface{} {
 92    parts := strings.Split(path, ".")
 93    current := data
 94
 95    for _, part := range parts {
 96        if part == "" {
 97            return nil
 98        }
 99
100        if value, ok := current[part]; ok {
101            if nextMap, ok := value.(map[string]interface{}); ok {
102                current = nextMap
103            } else {
104                return value
105            }
106        } else {
107            return nil
108        }
109    }
110
111    return current
112}
113
114func (te *TemplateEngine) interfaceToString(value interface{}) string {
115    switch v := value.(type) {
116    case string:
117        return v
118    case int, int64, float64:
119        return fmt.Sprintf("%v", v)
120    case bool:
121        if v {
122            return "true"
123        }
124        return "false"
125    default:
126        if value == nil {
127            return ""
128        }
129        return fmt.Sprintf("%v", v)
130    }
131}
132
133func FuzzTemplateEngine(f *testing.F) {
134    // Seed corpus
135    f.Add("Hello {{.name}}!")
136    f.Add("{{.user.name}} is {{.user.age}} years old")
137    f.Add("No placeholders here")
138    f.Add("{{.missing}}")
139
140    // Dangerous inputs
141    f.Add("{{.user.<script>alert('xss')</script>}}")
142    f.Add("{{.data:dangerous}}")
143
144    f.Fuzz(func(t *testing.T, template string) {
145        engine := NewTemplateEngine()
146
147        data := map[string]interface{}{
148            "name": "Test",
149            "user": map[string]interface{}{
150                "name": "Alice",
151                "age":  30,
152            },
153        }
154
155        result, err := engine.Execute(template, data)
156
157        // If execution succeeds, verify properties
158        if err == nil {
159            // 1. Result should not contain unprocessed templates
160            if strings.Contains(result, "{{.") {
161                t.Errorf("Result contains unprocessed template: %q", result)
162            }
163
164            // 2. Result should not be too large
165            if len(result) > 100000 {
166                t.Errorf("Result too large: %d characters", len(result))
167            }
168
169            // 3. Should not contain dangerous HTML
170            dangerous := []string{"<script", "javascript:", "data:"}
171            lowerResult := strings.ToLower(result)
172            for _, d := range dangerous {
173                if strings.Contains(lowerResult, d) {
174                    t.Errorf("Result contains dangerous content: %s", d)
175                }
176            }
177
178            // 4. Execution should be idempotent
179            result2, err2 := engine.Execute(template, data)
180            if err2 != nil {
181                t.Errorf("Second execution failed: %v", err2)
182            }
183            if result != result2 {
184                t.Errorf("Execution not idempotent")
185            }
186        }
187    })
188}
189
190func main() {
191    engine := NewTemplateEngine()
192
193    template := "Hello {{.user.name}}! You are {{.user.age}} years old."
194    data := map[string]interface{}{
195        "user": map[string]interface{}{
196            "name": "Alice",
197            "age":  30,
198        },
199    }
200
201    result, err := engine.Execute(template, data)
202    if err != nil {
203        fmt.Printf("Error: %v\n", err)
204    } else {
205        fmt.Printf("Result: %s\n", result)
206    }
207
208    // Test dangerous input
209    dangerous := "{{.user.<script>alert('xss')</script>}}"
210    _, err = engine.Execute(dangerous, nil)
211    if err != nil {
212        fmt.Printf("Dangerous input blocked: %v\n", err)
213    }
214}

Fuzzer Discoveries:
The fuzzer helps find:

  • Template injection vulnerabilities
  • Infinite recursion through circular references
  • Memory exhaustion through large outputs
  • XSS attacks through user input

Exercise 2: Binary Parser Fuzzer

Create a binary parser with proper security controls and fuzz it to find vulnerabilities.

Solution
  1package main
  2
  3import (
  4    "bytes"
  5    "encoding/binary"
  6    "errors"
  7    "fmt"
  8    "testing"
  9)
 10
 11// BinaryParser parses a custom binary format
 12type BinaryParser struct {
 13    maxElements int
 14    maxDepth   int
 15}
 16
 17type Record struct {
 18    ID    uint32
 19    Type  uint8
 20    Value []byte
 21}
 22
 23func NewBinaryParser() *BinaryParser {
 24    return &BinaryParser{
 25        maxElements: 1000,
 26        maxDepth:   10,
 27    }
 28}
 29
 30// Parse safely parses binary data into records
 31func (bp *BinaryParser) Parse(data []byte) ([]Record, error) {
 32    // Security check: Input size
 33    if len(data) > 1024*1024 {
 34        return nil, errors.New("input too large")
 35    }
 36
 37    if len(data) < 10 {
 38        return nil, errors.New("insufficient data for header")
 39    }
 40
 41    // Read header
 42    var header struct {
 43        Magic    uint32
 44        Version  uint8
 45        Count    uint16
 46        Reserved [3]uint8
 47    }
 48
 49    if err := binary.Read(bytes.NewReader(data[:10]), binary.BigEndian, &header); err != nil {
 50        return nil, fmt.Errorf("header parse error: %w", err)
 51    }
 52
 53    // Validate header
 54    if header.Magic != 0xDEADBEEF {
 55        return nil, errors.New("invalid magic number")
 56    }
 57
 58    if header.Version != 1 {
 59        return nil, fmt.Errorf("unsupported version: %d", header.Version)
 60    }
 61
 62    if int(header.Count) > bp.maxElements {
 63        return nil, fmt.Errorf("too many elements: %d", header.Count)
 64    }
 65
 66    // Calculate expected data size
 67    expectedSize := 10 // header size
 68    for i := uint16(0); i < header.Count; i++ {
 69        recordSize := 4 + 1 + 2 // ID + Type + ValueLength
 70        if int(expectedSize)+recordSize > len(data) {
 71            return nil, errors.New("insufficient data for records")
 72        }
 73
 74        // Read record header
 75        offset := int(expectedSize)
 76        valueLength := binary.BigEndian.Uint16(data[offset+5 : offset+7])
 77
 78        if valueLength > 1000 {
 79            return nil, fmt.Errorf("record value too large: %d", valueLength)
 80        }
 81
 82        if int(expectedSize)+recordSize+int(valueLength) > len(data) {
 83            return nil, errors.New("insufficient data for value")
 84        }
 85
 86        expectedSize += recordSize + int(valueLength)
 87    }
 88
 89    // Parse records
 90    var records []Record
 91    offset := 10
 92
 93    for i := uint16(0); i < header.Count; i++ {
 94        if offset+7 > len(data) {
 95            break
 96        }
 97
 98        id := binary.BigEndian.Uint32(data[offset : offset+4])
 99        recordType := data[offset+4]
100        valueLength := binary.BigEndian.Uint16(data[offset+5 : offset+7])
101
102        if int(offset)+7+int(valueLength) > len(data) {
103            break
104        }
105
106        value := make([]byte, valueLength)
107        copy(value, data[offset+7:offset+7+int(valueLength)])
108
109        records = append(records, Record{
110            ID:    id,
111            Type:  recordType,
112            Value: value,
113        })
114
115        offset += 7 + int(valueLength)
116    }
117
118    return records, nil
119}
120
121func FuzzBinaryParser(f *testing.F) {
122    // Seed corpus
123    f.Add(createValidBinary([]Record{{ID: 1, Type: 1, Value: []byte("hello")}}))
124    f.Add(createValidBinary([]Record{}))
125    f.Add([]byte{}) // Empty input
126
127    f.Fuzz(func(t *testing.T, data []byte) {
128        parser := NewBinaryParser()
129
130        // Should never panic
131        records, err := parser.Parse(data)
132
133        if err == nil {
134            // Verify invariants when parsing succeeds
135
136            // 1. Record count should be reasonable
137            if len(records) > 1000 {
138                t.Errorf("Too many records parsed: %d", len(records))
139            }
140
141            // 2. Each record should be valid
142            for i, record := range records {
143                if record.Type > 10 {
144                    t.Errorf("Record %d has invalid type: %d", i, record.Type)
145                }
146
147                if len(record.Value) > 1000 {
148                    t.Errorf("Record %d has value too long: %d", i, len(record.Value))
149                }
150            }
151
152            // 3. Should be able to reconstruct similar binary
153            if len(records) > 0 {
154                reconstructed := createValidBinary(records)
155                if len(reconstructed) > 100000 {
156                    t.Errorf("Reconstructed binary too large: %d", len(reconstructed))
157                }
158            }
159        }
160    })
161}
162
163func createValidBinary(records []Record) []byte {
164    // Helper to create valid binary format for testing
165    buf := make([]byte, 10)
166    binary.BigEndian.PutUint32(buf[0:4], 0xDEADBEEF)
167    buf[4] = 1 // version
168    binary.BigEndian.PutUint16(buf[5:7], uint16(len(records)))
169
170    for _, record := range records {
171        header := make([]byte, 7)
172        binary.BigEndian.PutUint32(header[0:4], record.ID)
173        header[4] = record.Type
174        binary.BigEndian.PutUint16(header[5:7], uint16(len(record.Value)))
175
176        buf = append(buf, header...)
177        buf = append(buf, record.Value...)
178    }
179
180    return buf
181}
182
183func main() {
184    parser := NewBinaryParser()
185
186    // Test valid binary
187    records := []Record{
188        {ID: 1, Type: 1, Value: []byte("hello")},
189        {ID: 2, Type: 2, Value: []byte("world")},
190    }
191
192    data := createValidBinary(records)
193    parsed, err := parser.Parse(data)
194    if err != nil {
195        fmt.Printf("Error: %v\n", err)
196    } else {
197        fmt.Printf("Parsed %d records:\n", len(parsed))
198        for _, record := range parsed {
199            fmt.Printf("  ID: %d, Type: %d, Value: %s\n",
200                record.ID, record.Type, record.Value)
201        }
202    }
203
204    // Test invalid magic number
205    invalid := make([]byte, 10)
206    binary.BigEndian.PutUint32(invalid[0:4], 0x12345678) // Wrong magic
207    _, err = parser.Parse(invalid)
208    if err != nil {
209        fmt.Printf("Invalid magic correctly rejected: %v\n", err)
210    }
211}

Exercise 3: URL Router Fuzzer

Build a URL router with parameter extraction and test it with fuzzing to ensure security and correctness.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "net/http"
  6    "net/http/httptest"
  7    "regexp"
  8    "strings"
  9    "testing"
 10)
 11
 12// Route represents a URL pattern with parameter extraction
 13type Route struct {
 14    Pattern string
 15    Regex   *regexp.Regexp
 16    Params  []string
 17    Handler http.HandlerFunc
 18}
 19
 20// Router manages URL routing with parameter extraction
 21type Router struct {
 22    routes []*Route
 23}
 24
 25func NewRouter() *Router {
 26    return &Router{routes: make([]*Route, 0)}
 27}
 28
 29// AddRoute adds a route with parameter extraction
 30// Pattern: /users/:id/posts/:postId
 31func (r *Router) AddRoute(pattern string, handler http.HandlerFunc) error {
 32    // Security: Validate pattern
 33    if len(pattern) > 1000 {
 34        return fmt.Errorf("pattern too long")
 35    }
 36
 37    if !strings.HasPrefix(pattern, "/") {
 38        return fmt.Errorf("pattern must start with /")
 39    }
 40
 41    // Extract parameter names
 42    params := make([]string, 0)
 43    regexPattern := "^"
 44
 45    parts := strings.Split(pattern, "/")
 46    for _, part := range parts {
 47        if part == "" {
 48            continue
 49        }
 50
 51        if strings.HasPrefix(part, ":") {
 52            paramName := part[1:]
 53            if paramName == "" {
 54                return fmt.Errorf("empty parameter name")
 55            }
 56            params = append(params, paramName)
 57            regexPattern += "/([^/]+)"
 58        } else {
 59            regexPattern += "/" + regexp.QuoteMeta(part)
 60        }
 61    }
 62
 63    regexPattern += "$"
 64
 65    regex, err := regexp.Compile(regexPattern)
 66    if err != nil {
 67        return fmt.Errorf("invalid pattern: %w", err)
 68    }
 69
 70    route := &Route{
 71        Pattern: pattern,
 72        Regex:   regex,
 73        Params:  params,
 74        Handler: handler,
 75    }
 76
 77    r.routes = append(r.routes, route)
 78    return nil
 79}
 80
 81// Match finds a matching route and extracts parameters
 82func (r *Router) Match(path string) (*Route, map[string]string, error) {
 83    // Security: Path length limit
 84    if len(path) > 2000 {
 85        return nil, nil, fmt.Errorf("path too long")
 86    }
 87
 88    for _, route := range r.routes {
 89        matches := route.Regex.FindStringSubmatch(path)
 90        if matches != nil {
 91            params := make(map[string]string)
 92            for i, paramName := range route.Params {
 93                if i+1 < len(matches) {
 94                    params[paramName] = matches[i+1]
 95                }
 96            }
 97            return route, params, nil
 98        }
 99    }
100
101    return nil, nil, fmt.Errorf("no route found")
102}
103
104// ServeHTTP implements http.Handler
105func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
106    route, params, err := r.Match(req.URL.Path)
107    if err != nil {
108        http.NotFound(w, req)
109        return
110    }
111
112    // Store params in request context or pass to handler
113    route.Handler(w, req)
114}
115
116func FuzzRouter(f *testing.F) {
117    // Seed corpus with various URL patterns
118    f.Add("/users")
119    f.Add("/users/123")
120    f.Add("/users/123/posts/456")
121    f.Add("/api/v1/resources")
122    f.Add("")
123    f.Add("/")
124    f.Add("//double//slash")
125    f.Add("/path/../traversal")
126    f.Add("/path/with spaces")
127    f.Add("/path%20encoded")
128    f.Add("/unicode/世界")
129    f.Add("/emoji/👍")
130
131    f.Fuzz(func(t *testing.T, path string) {
132        router := NewRouter()
133
134        // Add some routes
135        router.AddRoute("/users", func(w http.ResponseWriter, r *http.Request) {})
136        router.AddRoute("/users/:id", func(w http.ResponseWriter, r *http.Request) {})
137        router.AddRoute("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {})
138        router.AddRoute("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {})
139
140        // Should never panic on any input
141        route, params, err := router.Match(path)
142
143        if err == nil {
144            // If match succeeds, verify properties
145
146            // 1. Route should not be nil
147            if route == nil {
148                t.Error("Match succeeded but route is nil")
149            }
150
151            // 2. Params should match route parameter count
152            if len(params) != len(route.Params) {
153                t.Errorf("Param count mismatch: got %d, expected %d",
154                    len(params), len(route.Params))
155            }
156
157            // 3. Each param should be present
158            for _, paramName := range route.Params {
159                if _, ok := params[paramName]; !ok {
160                    t.Errorf("Missing parameter: %s", paramName)
161                }
162            }
163
164            // 4. Param values should not be empty for matched routes
165            for paramName, value := range params {
166                if value == "" {
167                    t.Errorf("Empty parameter value for: %s", paramName)
168                }
169
170                // Security: Param values should not contain path traversal
171                if strings.Contains(value, "..") {
172                    t.Errorf("Path traversal in param: %s", value)
173                }
174            }
175
176            // 5. Test actual HTTP request handling
177            req := httptest.NewRequest("GET", path, nil)
178            w := httptest.NewRecorder()
179
180            // Should not panic
181            router.ServeHTTP(w, req)
182
183            // Response should have valid status
184            if w.Code < 100 || w.Code > 599 {
185                t.Errorf("Invalid HTTP status: %d", w.Code)
186            }
187        }
188    })
189}
190
191func main() {
192    router := NewRouter()
193
194    // Add routes
195    router.AddRoute("/users", func(w http.ResponseWriter, r *http.Request) {
196        fmt.Fprintf(w, "List users")
197    })
198
199    router.AddRoute("/users/:id", func(w http.ResponseWriter, r *http.Request) {
200        fmt.Fprintf(w, "Get user")
201    })
202
203    router.AddRoute("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
204        fmt.Fprintf(w, "Get post")
205    })
206
207    // Test matching
208    testPaths := []string{
209        "/users",
210        "/users/123",
211        "/users/123/posts/456",
212        "/invalid",
213        "/users/123/posts",
214    }
215
216    fmt.Println("=== Route Matching Tests ===")
217    for _, path := range testPaths {
218        route, params, err := router.Match(path)
219        if err != nil {
220            fmt.Printf("%-30s -> No match\n", path)
221        } else {
222            fmt.Printf("%-30s -> Pattern: %s, Params: %v\n",
223                path, route.Pattern, params)
224        }
225    }
226}

Exercise 4: State Machine Fuzzer

Create a state machine with transitions and use fuzzing to find invalid state transitions or deadlocks.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "sync"
  6    "testing"
  7)
  8
  9// State represents a state in the machine
 10type State int
 11
 12const (
 13    StateInit State = iota
 14    StateRunning
 15    StatePaused
 16    StateStopped
 17    StateError
 18)
 19
 20func (s State) String() string {
 21    switch s {
 22    case StateInit:
 23        return "Init"
 24    case StateRunning:
 25        return "Running"
 26    case StatePaused:
 27        return "Paused"
 28    case StateStopped:
 29        return "Stopped"
 30    case StateError:
 31        return "Error"
 32    default:
 33        return "Unknown"
 34    }
 35}
 36
 37// Transition represents a state transition
 38type Transition int
 39
 40const (
 41    TransitionStart Transition = iota
 42    TransitionPause
 43    TransitionResume
 44    TransitionStop
 45    TransitionFail
 46    TransitionReset
 47)
 48
 49func (t Transition) String() string {
 50    switch t {
 51    case TransitionStart:
 52        return "Start"
 53    case TransitionPause:
 54        return "Pause"
 55    case TransitionResume:
 56        return "Resume"
 57    case TransitionStop:
 58        return "Stop"
 59    case TransitionFail:
 60        return "Fail"
 61    case TransitionReset:
 62        return "Reset"
 63    default:
 64        return "Unknown"
 65    }
 66}
 67
 68// StateMachine manages state transitions
 69type StateMachine struct {
 70    current State
 71    mu      sync.RWMutex
 72    history []State
 73}
 74
 75func NewStateMachine() *StateMachine {
 76    return &StateMachine{
 77        current: StateInit,
 78        history: []State{StateInit},
 79    }
 80}
 81
 82// GetState returns current state
 83func (sm *StateMachine) GetState() State {
 84    sm.mu.RLock()
 85    defer sm.mu.RUnlock()
 86    return sm.current
 87}
 88
 89// Transition attempts a state transition
 90func (sm *StateMachine) Transition(t Transition) error {
 91    sm.mu.Lock()
 92    defer sm.mu.Unlock()
 93
 94    oldState := sm.current
 95    newState, err := sm.getNextState(sm.current, t)
 96    if err != nil {
 97        return err
 98    }
 99
100    sm.current = newState
101    sm.history = append(sm.history, newState)
102
103    return nil
104}
105
106// getNextState determines next state based on current state and transition
107func (sm *StateMachine) getNextState(current State, t Transition) (State, error) {
108    switch current {
109    case StateInit:
110        switch t {
111        case TransitionStart:
112            return StateRunning, nil
113        case TransitionFail:
114            return StateError, nil
115        default:
116            return current, fmt.Errorf("invalid transition %s from %s", t, current)
117        }
118
119    case StateRunning:
120        switch t {
121        case TransitionPause:
122            return StatePaused, nil
123        case TransitionStop:
124            return StateStopped, nil
125        case TransitionFail:
126            return StateError, nil
127        default:
128            return current, fmt.Errorf("invalid transition %s from %s", t, current)
129        }
130
131    case StatePaused:
132        switch t {
133        case TransitionResume:
134            return StateRunning, nil
135        case TransitionStop:
136            return StateStopped, nil
137        case TransitionFail:
138            return StateError, nil
139        default:
140            return current, fmt.Errorf("invalid transition %s from %s", t, current)
141        }
142
143    case StateStopped:
144        switch t {
145        case TransitionReset:
146            return StateInit, nil
147        default:
148            return current, fmt.Errorf("invalid transition %s from %s", t, current)
149        }
150
151    case StateError:
152        switch t {
153        case TransitionReset:
154            return StateInit, nil
155        default:
156            return current, fmt.Errorf("invalid transition %s from %s", t, current)
157        }
158
159    default:
160        return current, fmt.Errorf("unknown state: %s", current)
161    }
162}
163
164// GetHistory returns state transition history
165func (sm *StateMachine) GetHistory() []State {
166    sm.mu.RLock()
167    defer sm.mu.RUnlock()
168    return append([]State{}, sm.history...)
169}
170
171func FuzzStateMachine(f *testing.F) {
172    // Seed with valid transition sequences
173    f.Add([]byte{0, 1, 2, 3})  // Start, Pause, Resume, Stop
174    f.Add([]byte{0, 3})        // Start, Stop
175    f.Add([]byte{4})           // Fail
176    f.Add([]byte{})            // Empty sequence
177
178    f.Fuzz(func(t *testing.T, transitionBytes []byte) {
179        // Skip extremely long sequences
180        if len(transitionBytes) > 100 {
181            return
182        }
183
184        sm := NewStateMachine()
185
186        // Track states visited
187        statesVisited := make(map[State]bool)
188        statesVisited[sm.GetState()] = true
189
190        // Apply transitions
191        for _, b := range transitionBytes {
192            // Map byte to valid transition
193            transition := Transition(int(b) % 6)
194
195            oldState := sm.GetState()
196            err := sm.Transition(transition)
197
198            newState := sm.GetState()
199            statesVisited[newState] = true
200
201            // Verify invariants
202            if err == nil {
203                // 1. State should have changed for valid transition
204                if newState == oldState && transition != TransitionStart {
205                    // Some transitions might keep same state, that's ok
206                }
207
208                // 2. New state should be valid
209                if newState < StateInit || newState > StateError {
210                    t.Errorf("Invalid state after transition: %d", newState)
211                }
212
213                // 3. History should have grown
214                history := sm.GetHistory()
215                if len(history) == 0 {
216                    t.Error("History is empty after transitions")
217                }
218
219                // 4. Last history entry should match current state
220                if history[len(history)-1] != newState {
221                    t.Errorf("History mismatch: last=%s, current=%s",
222                        history[len(history)-1], newState)
223                }
224            }
225        }
226
227        // Verify final state is valid
228        finalState := sm.GetState()
229        if finalState < StateInit || finalState > StateError {
230            t.Errorf("Invalid final state: %d", finalState)
231        }
232
233        // Verify we can always reach Init via Reset
234        if finalState == StateStopped || finalState == StateError {
235            err := sm.Transition(TransitionReset)
236            if err != nil {
237                t.Errorf("Cannot reset from terminal state %s: %v", finalState, err)
238            }
239
240            if sm.GetState() != StateInit {
241                t.Errorf("Reset did not return to Init: got %s", sm.GetState())
242            }
243        }
244    })
245}
246
247func main() {
248    sm := NewStateMachine()
249
250    fmt.Println("=== State Machine Demo ===")
251    fmt.Printf("Initial state: %s\n\n", sm.GetState())
252
253    // Valid transition sequence
254    transitions := []Transition{
255        TransitionStart,
256        TransitionPause,
257        TransitionResume,
258        TransitionStop,
259        TransitionReset,
260    }
261
262    for _, t := range transitions {
263        oldState := sm.GetState()
264        err := sm.Transition(t)
265
266        fmt.Printf("%s: %s", t, oldState)
267        if err != nil {
268            fmt.Printf(" -> ERROR: %v\n", err)
269        } else {
270            fmt.Printf(" -> %s ✓\n", sm.GetState())
271        }
272    }
273
274    fmt.Println("\n=== State History ===")
275    for i, state := range sm.GetHistory() {
276        fmt.Printf("%d. %s\n", i+1, state)
277    }
278}

Exercise 5: Compression Fuzzer

Build a simple compression/decompression system and fuzz it to ensure round-trip correctness and handle malformed data.

Solution
  1package main
  2
  3import (
  4    "bytes"
  5    "compress/gzip"
  6    "fmt"
  7    "io"
  8    "testing"
  9)
 10
 11// Compressor handles data compression with safety limits
 12type Compressor struct {
 13    maxInputSize  int
 14    maxOutputSize int
 15}
 16
 17func NewCompressor() *Compressor {
 18    return &Compressor{
 19        maxInputSize:  10 * 1024 * 1024,  // 10MB
 20        maxOutputSize: 100 * 1024 * 1024, // 100MB
 21    }
 22}
 23
 24// Compress compresses data with safety checks
 25func (c *Compressor) Compress(data []byte) ([]byte, error) {
 26    // Security: Input size limit
 27    if len(data) > c.maxInputSize {
 28        return nil, fmt.Errorf("input too large: %d bytes", len(data))
 29    }
 30
 31    var buf bytes.Buffer
 32    writer := gzip.NewWriter(&buf)
 33
 34    _, err := writer.Write(data)
 35    if err != nil {
 36        return nil, fmt.Errorf("compression failed: %w", err)
 37    }
 38
 39    if err := writer.Close(); err != nil {
 40        return nil, fmt.Errorf("compression finalize failed: %w", err)
 41    }
 42
 43    compressed := buf.Bytes()
 44
 45    // Security: Output size check
 46    if len(compressed) > c.maxOutputSize {
 47        return nil, fmt.Errorf("compressed data too large: %d bytes", len(compressed))
 48    }
 49
 50    return compressed, nil
 51}
 52
 53// Decompress decompresses data with safety checks
 54func (c *Compressor) Decompress(data []byte) ([]byte, error) {
 55    // Security: Input size limit
 56    if len(data) > c.maxInputSize {
 57        return nil, fmt.Errorf("compressed data too large: %d bytes", len(data))
 58    }
 59
 60    reader, err := gzip.NewReader(bytes.NewReader(data))
 61    if err != nil {
 62        return nil, fmt.Errorf("decompression init failed: %w", err)
 63    }
 64    defer reader.Close()
 65
 66    // Use limited reader to prevent decompression bombs
 67    limitedReader := io.LimitReader(reader, int64(c.maxOutputSize))
 68
 69    var buf bytes.Buffer
 70    _, err = io.Copy(&buf, limitedReader)
 71    if err != nil {
 72        return nil, fmt.Errorf("decompression failed: %w", err)
 73    }
 74
 75    decompressed := buf.Bytes()
 76
 77    // Check if we hit the limit (decompression bomb)
 78    if len(decompressed) == c.maxOutputSize {
 79        return nil, fmt.Errorf("decompressed data exceeds size limit")
 80    }
 81
 82    return decompressed, nil
 83}
 84
 85func FuzzCompressor(f *testing.F) {
 86    // Seed corpus with various data patterns
 87    f.Add([]byte("hello world"))
 88    f.Add([]byte(""))
 89    f.Add([]byte("a"))
 90    f.Add(bytes.Repeat([]byte("A"), 1000))  // Highly compressible
 91    f.Add([]byte{0, 1, 2, 3, 4, 5, 6, 7})   // Binary data
 92
 93    // Random-looking data (low compressibility)
 94    random := make([]byte, 100)
 95    for i := range random {
 96        random[i] = byte(i * 7 % 256)
 97    }
 98    f.Add(random)
 99
100    f.Fuzz(func(t *testing.T, data []byte) {
101        // Skip very large inputs to keep fuzzing fast
102        if len(data) > 100000 {
103            return
104        }
105
106        compressor := NewCompressor()
107
108        // Compress
109        compressed, compressErr := compressor.Compress(data)
110
111        if compressErr == nil {
112            // Property 1: Compressed data should not be too large
113            if len(compressed) > len(data)*2+1000 {
114                t.Errorf("Compression inefficient: %d -> %d bytes",
115                    len(data), len(compressed))
116            }
117
118            // Property 2: Should be able to decompress
119            decompressed, decompressErr := compressor.Decompress(compressed)
120
121            if decompressErr != nil {
122                t.Errorf("Decompression failed after successful compression: %v",
123                    decompressErr)
124            } else {
125                // Property 3: Round-trip should preserve data
126                if !bytes.Equal(data, decompressed) {
127                    t.Errorf("Round-trip failed: original %d bytes, got %d bytes",
128                        len(data), len(decompressed))
129                }
130
131                // Property 4: Double compression should work
132                compressed2, err := compressor.Compress(decompressed)
133                if err != nil {
134                    t.Errorf("Second compression failed: %v", err)
135                }
136
137                // Property 5: Double decompression should work
138                if compressed2 != nil {
139                    decompressed2, err := compressor.Decompress(compressed2)
140                    if err != nil {
141                        t.Errorf("Second decompression failed: %v", err)
142                    }
143
144                    if !bytes.Equal(data, decompressed2) {
145                        t.Error("Double round-trip failed")
146                    }
147                }
148            }
149        }
150
151        // Test decompression of arbitrary data (should handle gracefully)
152        _, decompressErr := compressor.Decompress(data)
153        // It's OK if this fails, just shouldn't panic
154        _ = decompressErr
155    })
156}
157
158func main() {
159    compressor := NewCompressor()
160
161    testCases := []struct {
162        name string
163        data []byte
164    }{
165        {"Empty", []byte{}},
166        {"Small text", []byte("hello world")},
167        {"Repeated", bytes.Repeat([]byte("A"), 1000)},
168        {"Binary", []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
169    }
170
171    fmt.Println("=== Compression Tests ===")
172    for _, tc := range testCases {
173        compressed, err := compressor.Compress(tc.data)
174        if err != nil {
175            fmt.Printf("%-20s: Compression failed: %v\n", tc.name, err)
176            continue
177        }
178
179        decompressed, err := compressor.Decompress(compressed)
180        if err != nil {
181            fmt.Printf("%-20s: Decompression failed: %v\n", tc.name, err)
182            continue
183        }
184
185        ratio := float64(len(compressed)) / float64(len(tc.data))
186        if len(tc.data) == 0 {
187            ratio = 0
188        }
189
190        match := "✓"
191        if !bytes.Equal(tc.data, decompressed) {
192            match = "✗"
193        }
194
195        fmt.Printf("%-20s: %d -> %d bytes (%.2f%%) %s\n",
196            tc.name, len(tc.data), len(compressed), ratio*100, match)
197    }
198}

Further Reading

Official Documentation

Books and Papers

  • The Fuzzing Book - Comprehensive guide to fuzzing techniques
  • Fuzzing: Brute Force Vulnerability Discovery - Security-focused approach
  • Fuzzing Research Papers - Academic research

Tools and Services

  • OSS-Fuzz - Free continuous fuzzing for open source
  • AFL - Advanced fuzzy testing
  • LibFuzzer - LLVM's fuzzing engine

Security Resources

Summary

Key Takeaways

  1. Fuzzing discovers bugs unit tests miss by exploring unexpected inputs
  2. Property-based testing is more effective than testing specific outcomes
  3. Go's native fuzzing makes comprehensive testing accessible
  4. Security-focused fuzzing prevents production vulnerabilities
  5. Integration with CI/CD provides continuous protection

When to Use Fuzz Testing

Perfect for:

  • Input parsers (JSON, XML, binary formats)
  • String processing and template engines
  • Network protocol handlers
  • Authentication and validation logic
  • Serialization/deserialization code
  • Regular expression engines
  • State machines and workflow systems

⚠️ Less effective for:

  • Pure mathematical computations
  • Simple CRUD database operations
  • Configuration-only code
  • External API integration tests

Mastery Path

Beginner: Write basic fuzz tests for simple functions
Intermediate: Implement property-based testing with invariants
Advanced: Create complex fuzz targets for parsers and protocols
Expert: Integrate continuous fuzzing with corpus management

Next Steps:

  1. Add fuzz tests to your current project
  2. Set up automated fuzzing in CI/CD
  3. Learn from OSS-Fuzz success stories
  4. Explore advanced fuzzing techniques

Remember: A single fuzz test that finds one critical vulnerability is worth more than a hundred unit tests that verify expected behavior.