commandby evmts

Fuzz Testing

Run fuzz tests using Zig's built-in fuzzer (Linux-only, requires Docker on macOS).

Installs: 0
Used in: 1 repos
Updated: 3h ago
$npx ai-builder add command evmts/fuzz

Installs to .claude/commands/fuzz.md

# Fuzz Testing

Run fuzz tests using Zig's built-in fuzzer (Linux-only, requires Docker on macOS).

## Overview

Zig has integrated fuzz testing since 0.14.0 (merged July 2024). Uses LibFuzzer under the hood with in-process fuzzing and coverage-guided test generation.

**Platform Requirements:**
- **Linux**: Native support
- **macOS**: Not supported (InvalidElfMagic error - assumes ELF format, needs MachO glue code). Use Docker.
- **Windows**: Not documented

## Quick Start

### Basic Usage (Linux)

```bash
zig build test --fuzz              # Run indefinitely
zig build test --fuzz=300s         # Run for 5 minutes
zig build test --fuzz --debug-rt   # Debug mode (compiler_rt + libfuzzer)
zig build test --fuzz --port=8080  # Custom port for web UI
```

### Docker Usage (macOS/Cross-platform)

```bash
# Run fuzzing in Linux container
docker run --rm -it -v $(pwd):/workspace -w /workspace \
  ziglang/zig:0.15.1 \
  zig build test --fuzz=300s

# With web interface (map port)
docker run --rm -it -v $(pwd):/workspace -w /workspace \
  -p 6971:6971 \
  ziglang/zig:0.15.1 \
  zig build test --fuzz --port=6971
```

Access web UI: http://localhost:6971

## Writing Fuzz Tests

### Basic Pattern

```zig
test "fuzz example" {
    const input_bytes = std.testing.fuzzInput(.{});
    // Test code that processes input_bytes
    try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input_bytes));
}
```

### Real-World Example: Parser/Tokenizer

```zig
test "tokenizer fuzzing" {
    const source = std.testing.fuzzInput(.{});

    var index: usize = 0;
    while (index < source.len) {
        const token, const length = lexOne(source[index..]);
        index += length;

        // Validate token properties
        try std.testing.expect(length > 0);
        try std.testing.expect(index <= source.len);
    }
}
```

### Crypto/Security Example

```zig
test "fuzz address validation" {
    const input = std.testing.fuzzInput(.{});

    // Should never panic, only return error
    const result = Address.from(input) catch |err| {
        try std.testing.expect(err == error.InvalidLength or
                               err == error.InvalidFormat);
        return;
    };

    // If valid, verify invariants
    try std.testing.expect(result.len == 20);
}
```

## Advanced Patterns

### Memory Allocation Testing

```zig
test "fuzz with allocation limits" {
    const input = std.testing.fuzzInput(.{});

    // Custom allocator that panics on oversized allocations
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    var max_size_allocator = MaxSizeAllocator{
        .parent = arena.allocator(),
        .max_size = input.len * 10, // Reasonable multiple of input
    };

    // Parse with allocation limit
    _ = parse(max_size_allocator.allocator(), input) catch return;
}
```

### Allocation Failure Testing

```zig
test "fuzz allocation failures" {
    const input = std.testing.fuzzInput(.{});

    // Generate deterministic failure index from input
    var hash = std.hash.Wyhash.init(0);
    hash.update(input);

    // First pass: count allocations
    var counting_alloc = std.testing.allocator_instance;
    const initial_count = counting_alloc.total_requested_bytes;
    _ = parse(counting_alloc.allocator(), input) catch return;
    const alloc_count = counting_alloc.total_requested_bytes - initial_count;

    // Second pass: fail at specific allocation
    const fail_index = hash.final() % (alloc_count + 1);
    var failing_alloc = std.testing.FailingAllocator.init(
        std.testing.allocator,
        .{ .fail_index = fail_index },
    );

    _ = parse(failing_alloc.allocator(), input) catch return;
}
```

### Structured Input Fuzzing

```zig
test "fuzz structured data" {
    const raw_input = std.testing.fuzzInput(.{});
    if (raw_input.len < 4) return; // Need minimum bytes

    // Extract structured fields from raw bytes
    const field_a = raw_input[0];
    const field_b = std.mem.readInt(u16, raw_input[1..3], .little);
    const field_c = raw_input[3..];

    // Test with structured data
    const result = processStructured(field_a, field_b, field_c);
    try std.testing.expect(result.isValid());
}
```

## Corpus Management

### Corpus Location

```
.zig-cache/f/{test_name}/{index}
```

- `{test_name}`: Name of the fuzz test
- `{index}`: Incrementing integer for each interesting input

### Corpus Structure

- **Coverage DB**: `.zig-cache/v/{coverage_id}` (hex-encoded 64-bit ID)
- **Input Files**: Numbered sequentially (0, 1, 2, ...)
- **Shared Input**: Single file `in` used during fuzzing

### Seeding Corpus (Planned Feature)

Not yet implemented. Track: https://github.com/ziglang/zig/issues/20814

```zig
// Future API (not available yet)
test "fuzz with seeds" {
    const input = std.testing.fuzzInput(.{
        .corpus = &[_][]const u8{
            "seed1",
            "seed2",
            &[_]u8{0xFF, 0xFE, 0xFD}, // Binary seed
        },
    });
}
```

## Web Interface

When `--fuzz` is passed, build runner starts HTTP server on port 6971 (configurable with `--port`).

### Features

- **Live Coverage Map**: Red/green dots inline with source code
- **Real-time Updates**: WebSocket for live fuzzing stats
- **PC Tracking**: Shows which program counter addresses have coverage
- **Progress Stats**: Runs, crashes, hangs, corpus size

### Access

```bash
# Default port
zig build test --fuzz
# Visit: http://localhost:6971

# Custom port
zig build test --fuzz --port=8080
# Visit: http://localhost:8080
```

## Compiler Flags

### Fuzz-Specific Flags

- `-ffuzz`: Enable fuzzing instrumentation
- `-fno-fuzz`: Disable fuzzing (default)

**Implementation**: Passes `-fsanitize=fuzzer-no-link` to Clang for C/C++ files

### Related Flags

```bash
--debug-rt              # Debug mode for compiler_rt and libfuzzer
--port <port>           # Web interface port (default: 6971)
-Dtest-filter=<name>    # Run only matching fuzz tests
```

## build.zig Integration

### Add Fuzz Test Step

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Main build
    const lib = b.addStaticLibrary(.{
        .name = "mylib",
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Tests (includes fuzz tests)
    const tests = b.addTest(.{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });

    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&b.addRunArtifact(tests).step);

    // Fuzz step (Linux-only warning)
    const fuzz_step = b.step("fuzz", "Run fuzz tests (Linux only, use Docker on macOS)");
    fuzz_step.dependOn(test_step);
}
```

### Conditional Docker Execution

```zig
pub fn build(b: *std.Build) void {
    // ... standard build setup ...

    const fuzz_step = b.step("fuzz", "Run fuzz tests");

    // Detect platform
    const target_info = @import("builtin").os.tag;
    const is_linux = target_info == .linux;

    if (!is_linux) {
        // Provide Docker command for non-Linux
        const docker_cmd = b.addSystemCommand(&[_][]const u8{
            "docker", "run", "--rm", "-it",
            "-v", "$(pwd):/workspace",
            "-w", "/workspace",
            "-p", "6971:6971",
            "ziglang/zig:0.15.1",
            "zig", "build", "test", "--fuzz=300s",
        });
        fuzz_step.dependOn(&docker_cmd.step);
    } else {
        fuzz_step.dependOn(test_step);
    }
}
```

## Best Practices

### 1. Keep Tests Simple

```zig
// ✅ Good: Single entry point
test "fuzz parse" {
    const input = std.testing.fuzzInput(.{});
    _ = parse(input) catch return;
}

// ❌ Bad: Multiple code paths
test "fuzz everything" {
    const input = std.testing.fuzzInput(.{});
    if (input.len > 100) parseA(input) else parseB(input);
}
```

### 2. Validate Invariants, Not Implementation

```zig
// ✅ Good: Property-based
test "fuzz serialization roundtrip" {
    const input = std.testing.fuzzInput(.{});
    const parsed = parse(input) catch return;
    const serialized = serialize(parsed);
    const reparsed = parse(serialized) catch unreachable;
    try std.testing.expectEqual(parsed, reparsed);
}

// ❌ Bad: Specific expected output
test "fuzz parse" {
    const input = std.testing.fuzzInput(.{});
    const result = parse(input) catch return;
    try std.testing.expectEqual(42, result.value); // Too specific
}
```

### 3. Handle Expected Errors Gracefully

```zig
// ✅ Good: Differentiate expected errors
test "fuzz decode" {
    const input = std.testing.fuzzInput(.{});

    const result = decode(input) catch |err| switch (err) {
        // Expected errors - not bugs
        error.InvalidFormat,
        error.InvalidLength,
        error.InvalidChecksum => return,

        // Unexpected errors - let fuzzer catch them
        else => return err,
    };

    // Validate successful decode
    try std.testing.expect(result.isValid());
}
```

### 4. Use Deterministic Operations

```zig
// ✅ Good: No randomness/time dependencies
test "fuzz hash" {
    const input = std.testing.fuzzInput(.{});
    const hash1 = computeHash(input);
    const hash2 = computeHash(input);
    try std.testing.expectEqual(hash1, hash2);
}

// ❌ Bad: Non-deterministic
test "fuzz timestamp" {
    const input = std.testing.fuzzInput(.{});
    const result = processWithTimestamp(input, std.time.timestamp());
    // Results vary between runs
}
```

### 5. Scope errdefer Properly

```zig
// ✅ Good: errdefer scoped to ownership
test "fuzz allocations" {
    const input = std.testing.fuzzInput(.{});
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    const data = try arena.allocator().dupe(u8, input);
    // No errdefer needed - arena owns cleanup

    try process(data);
}

// ❌ Bad: errdefer after ownership transfer
const value = try alloc.dupe(u8, input);
errdefer alloc.free(value); // Might double-free if list owns it
try list.append(alloc, value); // Transfers ownership
```

## Performance Tips

### 1. Use AFL_HANG_TMOUT for Timeouts

```bash
# Detect hangs after 10ms
AFL_HANG_TMOUT=10 zig build test --fuzz
```

### 2. Limit Input Size

```zig
test "fuzz bounded" {
    const input = std.testing.fuzzInput(.{});
    if (input.len > 1024) return; // Skip oversized

    try process(input);
}
```

### 3. Early Exit on Invalid Input

```zig
test "fuzz validate early" {
    const input = std.testing.fuzzInput(.{});

    // Quick checks first
    if (input.len < 4) return;
    if (input[0] != 0x89) return; // Magic byte

    // Expensive parsing only for valid-looking input
    _ = parseExpensive(input) catch return;
}
```

## Common Issues

### 1. macOS "InvalidElfMagic" Error

**Problem**: `zig build test --fuzz` fails on macOS with ELF format error.

**Solution**: Use Docker (see Docker Usage above).

**Root Cause**: Fuzzer assumes ELF debug format, macOS uses MachO. Fix requires glue code in `lib/std/debug/Info.zig`.

### 2. Out of Memory

**Problem**: Fuzzer consumes excessive memory with large inputs.

**Solution**: Limit input size or use custom allocators.

```zig
test "fuzz memory limited" {
    const input = std.testing.fuzzInput(.{});
    if (input.len > 4096) return; // Skip large inputs

    var buffer: [4096]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    _ = parse(fba.allocator(), input) catch return;
}
```

### 3. Timeouts

**Problem**: Fuzzer hangs on certain inputs.

**Solution**: Set timeout with `--fuzz=<duration>` or use `AFL_HANG_TMOUT`.

```bash
zig build test --fuzz=60s  # Exit after 60 seconds
```

### 4. False Positives

**Problem**: Fuzzer finds "bugs" in intentionally unsafe code.

**Solution**: Wrap unsafe operations with error handling.

```zig
test "fuzz unsafe ops" {
    const input = std.testing.fuzzInput(.{});

    // Validate before unsafe operation
    if (!isValidForUnsafeOp(input)) return;

    unsafeOperation(input);
}
```

## CI Integration

### GitHub Actions

```yaml
name: Fuzz Tests

on:
  schedule:
    - cron: '0 2 * * *' # Daily at 2 AM
  workflow_dispatch:

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Zig
        uses: goto-bus-stop/setup-zig@v2
        with:
          version: 0.15.1

      - name: Run Fuzz Tests (5 minutes each)
        run: zig build test --fuzz=300s

      - name: Upload Crash Corpus
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: fuzz-corpus-crash
          path: .zig-cache/f/
```

### Docker CI

```dockerfile
FROM ziglang/zig:0.15.1

WORKDIR /workspace
COPY . .

# Run fuzzing for 10 minutes
RUN zig build test --fuzz=600s

# Preserve corpus
RUN cp -r .zig-cache/f /corpus-output
```

## Limitations

### Current (Zig 0.15.1)

- **No corpus seeding**: Can't provide initial test cases
- **No length range**: Can't constrain input size declaratively
- **macOS unsupported**: Requires Docker/Linux
- **Basic algorithms**: Simple mutation strategies
- **No parallel fuzzing**: Single-threaded only
- **No persistent mode**: Restarts process for each run

### Future Enhancements (Planned)

- Corpus support (Issue #20814)
- Shared memory for inputs (PR #22862)
- Length range options
- Enhanced mutation algorithms
- Parallel fuzzing
- Time limits per test
- Crash minimization
- Dictionary support

## Resources

### Documentation

- **Zig PR #20773**: Initial integration https://github.com/ziglang/zig/pull/20773
- **Zig PR #20958**: Web interface https://github.com/ziglang/zig/pull/20958
- **macOS Support**: Issue #20986 https://github.com/ziglang/zig/issues/20986
- **Corpus Support**: Issue #20814 https://github.com/ziglang/zig/issues/20814

### Blog Posts

- **Ryan Liptak - Improving Fuzz Testing**: https://www.ryanliptak.com/blog/improving-fuzz-testing-with-zig-allocators/
- **Ryan Liptak - AFL++ Integration**: https://www.ryanliptak.com/blog/fuzzing-zig-code/
- **Phoenix K - Using Zig's Fuzzer**: https://phoenixk.net/zig-fuzz
- **Garrett Squire - Start Fuzzing**: https://gsquire.github.io/static/post/start-fuzzing-with-zig/

### Examples

- **Zig Std Lib Fuzzing**: https://github.com/squeek502/zig-std-lib-fuzzing
- **Fuzzi Utility Library**: https://github.com/steelcake/fuzzi

## API Status

⚠️ **Alpha Quality**: Fuzzing API is experimental and subject to change.

Current API (`std.testing.fuzzInput`) is stable in 0.15.1 but may evolve in future releases.

## Summary

Zig's built-in fuzzer provides coverage-guided testing with minimal boilerplate. Best used for:

- **Parsers/Decoders**: Find edge cases in input processing
- **Crypto**: Validate constant-time operations, no panics
- **Serialization**: Roundtrip property testing
- **Allocators**: Memory leak/corruption detection
- **Security**: Boundary validation, overflow checks

**On macOS**: Use Docker for Linux target. On Linux: Native support works well.

**Fuzzing mindset**: Properties, not examples. Invariants, not implementations. Crashes, not coverage.

Quick Install

$npx ai-builder add command evmts/fuzz

Details

Type
command
Author
evmts
Slug
evmts/fuzz
Created
3h ago