Skip to content

Adding Mutation Strategies

Crucible's mutation engine is designed for extensibility. Each mutation strategy implements a simple interface and is registered in a category with a selection weight.

The Strategy Interface

type Strategy interface {
    Name() string
    Mutate(f *gguf.File, rng *rand.Rand)
}

Every strategy must:

  1. Name() — Return a unique dot-separated name like metadata.my_mutation
  2. Mutate() — Modify the gguf.File in place using the provided RNG for randomness

Step-by-Step: Adding a New Strategy

1. Choose a Category

Category Weight When to use
CategoryHeader 10% Targets header fields (magic, version, counts)
CategoryMetadata 35% Targets metadata KV pairs
CategoryTensorInfo 35% Targets tensor info blocks
CategoryAlignment 5% Targets alignment/padding
CategoryData 5% Targets tensor data section
CategoryConsistency 10% Cross-field mismatches

2. Implement the Strategy

Create your strategy in the appropriate file under pkg/mutator/:

pkg/mutator/metadata.go
// metadataUnicodeExploitMutator injects Unicode edge cases
// that may confuse string length calculations.
type metadataUnicodeExploitMutator struct{}

func (m *metadataUnicodeExploitMutator) Name() string {
    return "metadata.unicode_exploit"
}

func (m *metadataUnicodeExploitMutator) Mutate(f *gguf.File, rng *rand.Rand) {
    idx := randMetadataIdx(f, rng)

    // Strings where byte length != character count
    payloads := []string{
        "\xc0\x80",                    // Overlong null
        "\xed\xa0\x80\xed\xb0\x80",   // Surrogate pair
        "\xef\xbb\xbf" + "normal",    // BOM prefix
    }
    f.Metadata[idx].Key = payloads[rng.IntN(len(payloads))]
}

3. Register the Strategy

Add it to the factory function in the same file:

pkg/mutator/metadata.go
func MetadataStrategies() []Strategy {
    return []Strategy{
        // ... existing strategies ...
        &metadataUnicodeExploitMutator{}, // (1)!
    }
}
  1. The mutator orchestrator automatically picks it up through registerDefaults().

That's it — no other registration needed. The orchestrator discovers strategies through the factory functions.

4. Test It

pkg/mutator/mutator_test.go
func TestUnicodeExploit(t *testing.T) {
    f := createTestFile()
    s := &metadataUnicodeExploitMutator{}
    rng := rand.New(rand.NewPCG(42, 42))

    s.Mutate(f, rng)

    // Verify mutation was applied
    // (check that a key contains non-UTF8 bytes, etc.)
    _, err := gguf.Marshal(f)
    if err != nil {
        t.Logf("marshal error (may be expected): %v", err)
    }
}

Run tests:

make test

Helper Functions

The mutator package provides helpers for common patterns:

Helper Purpose
ensureMetadata(f, rng) Add a dummy metadata entry if none exist
ensureTensor(f, rng) Add a dummy tensor if none exist
randMetadataIdx(f, rng) Get a random metadata index (creates one if empty)
randTensorIdx(f, rng) Get a random tensor index (creates one if empty)

Design Principles

Mutation Design Tips

  • Target real parser code paths: Study the target source (e.g., gguf.c) to understand what values trigger interesting behavior
  • Use edge values: 0, 1, MAX, off-by-one from boundaries
  • Create mismatches: The worst bugs come from fields that disagree with each other
  • Keep mutations small: 1-3 targeted changes per file find more bugs than total corruption
  • Use the RNG: Always use the provided rng parameter for deterministic reproduction