Mutation Strategies¶
Crucible ships 60 mutation strategies across two structure-aware mutators: 46 targeting GGUF file format parsing (6 categories) and 14 targeting RPC GRAPH_COMPUTE wire format (6 categories). Each strategy targets a specific structural component to trigger a known class of bugs.
Category weights
The mutator selects categories using weighted random sampling: Metadata (35%), TensorInfo (35%), Header (10%), Consistency (10%), Alignment (5%), Data (5%). Higher weights target sections with the most parser complexity and historical bug density.
Header Strategies¶
Target the 24-byte fixed header that every GGUF parser reads first.
| Strategy | Description | Bug Pattern |
|---|---|---|
header.version | Fuzz version field: 0, 1, 2, UINT32_MAX, random | Version handling bugs |
header.tensor_count | Mismatch count with actual tensor info blocks | Buffer overflow on allocation |
header.metadata_kv_count | Mismatch count with actual KV pairs | Over-read past metadata section |
header.magic_corrupt | Partial magic corruption (keep 1-2 valid bytes) | Error path bugs |
header.version_mismatch | Set version that disagrees with field sizes used | Version-dependent parsing bugs |
Metadata Strategies¶
Target the variable-length key-value section. This is the largest attack surface in most GGUF parsers.
| Strategy | Description | Bug Pattern |
|---|---|---|
metadata.key_length | Empty keys, 1MB+ keys, embedded null bytes, declared-length mismatches (encoded length != actual key bytes) | String handling overflow, parser field desync |
metadata.key_content | Non-UTF8 sequences, null sleds, path traversal strings, surrogate pairs | Encoding bugs |
metadata.value_type | Invalid enum values (14+, UINT32_MAX), type confusion between similar types | Type confusion / wrong getter |
metadata.string_value | Empty strings, 10MB strings, embedded nulls, non-UTF8 | String processing overflow |
metadata.array | Empty arrays, nested arrays, element type mismatch, large arrays (100K+ elements) | Array handling overflow |
metadata.int_overflow | UINT32_MAX, UINT64_MAX, INT64_MIN in integer fields | Integer overflow |
metadata.alignment_poison | Set general.alignment to 0, 1, 3, 7, UINT32_MAX | Division by zero, huge allocation |
metadata.key_shadow | Duplicate keys like general.architecture with conflicting value types | Duplicate key confusion |
metadata.add_extra | Inject 50-250 extra KV pairs with random types and large values | Parser stress / OOM |
metadata.invalid_utf8 | Inject non-UTF-8 byte sequences in string values | Encoding validation bugs |
metadata.reorder | Randomize the order of metadata key-value pairs | Order-dependent parsing bugs |
metadata.deep_array | Create arrays nested to extreme depth; arrays whose declared element count exceeds actual data (1M declared, 2 emitted) | Stack overflow in recursive parsing, read past end of array |
metadata.string_truncated | Declared string length exceeds actual bytes available (via on-wire length override) | Read past end of metadata section |
TensorInfo Strategies¶
Target the per-tensor descriptor blocks that parsers use to locate and allocate tensor data.
| Strategy | Description | Bug Pattern |
|---|---|---|
tensorinfo.n_dims | Set dimensions to 0, 5+, UINT32_MAX (spec allows 1-4) | Out-of-bounds dimension read |
tensorinfo.dim_overflow | Set individual dimension values to 0 or UINT64_MAX | Allocation size bugs |
tensorinfo.type | Invalid ggml_type enum values (5, 15, 255, UINT32_MAX) | Type lookup crash |
tensorinfo.offset | Offset beyond file size, UINT64_MAX, overlapping with other tensors | Out-of-bounds read |
tensorinfo.name | Empty names, 1MB names, embedded nulls, non-UTF8, duplicate names | String overflow, dedup bugs |
tensorinfo.dim_product_overflow | Dimension values whose product overflows uint64 | Undersized alloc + oversized read/write |
tensorinfo.name_collision | Give two tensors the same name | Deduplication and lookup bugs |
tensorinfo.offset_wraparound | Offset + size wraps uint64, bypassing bounds checks | Out-of-bounds memory access |
Alignment Strategies¶
Target the padding calculations between the metadata section and tensor data.
| Strategy | Description | Bug Pattern |
|---|---|---|
alignment.padding | Set alignment to 0, prime numbers, UINT32_MAX, OS page size | Padding calculation crash |
alignment.extra_padding | Insert random non-zero bytes before tensor data section | Offset miscalculation |
alignment.missing_padding | Metadata claims alignment but padding bytes are absent | Missing padding handling |
Data Strategies¶
Target the raw tensor data blob at the end of the file.
| Strategy | Description | Bug Pattern |
|---|---|---|
data.truncate | Truncate data section mid-tensor | Read past end of file |
data.overlap | Multiple tensors pointing to the same offset | Double-read, data confusion |
data.zero_length | Empty data section with non-zero tensor count | Null pointer / zero-size alloc |
data.shorter | Data section shorter than the sum of all tensor sizes | Partial read overflow |
data.garbage_fill | Fill data section with random bytes | Data corruption handling |
data.nan_inf | Inject NaN and Infinity values into tensor data | Special float handling bugs |
Consistency Strategies¶
Violate cross-section invariants that parsers may assume hold true.
| Strategy | Description | Bug Pattern |
|---|---|---|
consistency.tensor_count | Header tensor count != actual tensor info block count | Over-read / under-read |
consistency.metadata_count | Header metadata count != actual KV pair count | Parser desync |
consistency.offset_beyond | Tensor offset + tensor data size > total file size | Out-of-bounds read |
consistency.tensor_size | Dimensions claim X bytes but actual data region is Y bytes | Size mismatch overflow |
consistency.duplicate_offset | Multiple tensors claim the same offset range | Aliased memory access |
consistency.alignment_disagree | Metadata alignment value != actual file padding alignment | Offset miscalculation |
Model-Loader Strategies¶
Target the model-loading path (llama_model_load) beyond raw GGUF parsing. These strategies mutate architecture keys, hyperparameters, vocabulary fields, and layer counts to trigger integer overflows and assertion failures in the model loading and architecture dispatch code.
Category weighting
Model-loader strategies are registered under the Metadata category for weighting purposes (35% selection probability). They share the metadata budget because they operate on metadata key-value pairs that feed into model initialization.
| Strategy | Description | Bug Pattern |
|---|---|---|
model.architecture | Set general.architecture to unknown, empty, or malformed values (null bytes, path traversal strings) | Architecture dispatch crash, unhandled enum |
model.hyperparam | Set hyperparameter keys (embedding_length, head_count, block_count, etc.) to 0, UINT32_MAX, or other boundary values | Integer overflow in allocation sizing |
model.vocab | Mutate tokenizer keys (tokenizer.ggml.model, bos_token_id, eos_token_id) with invalid strings or extreme integer values | Tokenizer initialization crash |
model.layer_count | Set block_count to 0, UINT32_MAX, or values mismatched with actual tensor count | Layer iteration overflow, assertion failure |
model.tensor_name_schema | Corrupt tensor names to break the blk.N.attn_q.weight naming convention parsers rely on | Tensor lookup failure, null dereference |
Strategy Summary¶
| Category | Count | Weight | Primary Target |
|---|---|---|---|
| Header | 5 | 10% | Fixed header fields |
| Metadata | 13 | 35% | Key-value parsing |
| TensorInfo | 8 | 35% | Tensor descriptor blocks |
| Alignment | 3 | 5% | Padding between sections |
| Data | 6 | 5% | Raw tensor data blob |
| Consistency | 6 | 10% | Cross-section invariants |
| Model-Loader | 5 | (shared with Metadata) | Model initialization path |
| Total | 46 | 100% |
Composability
The mutator may apply multiple strategies in a single pass. Combined mutations (e.g., header.tensor_count + consistency.offset_beyond) often trigger bugs that no single mutation would reach alone.
Header count synchronization
By default, the mutator calls SyncCounts() after all mutations to ensure Header.MetadataKVCount and Header.TensorCount match the actual slice lengths. Strategies that append metadata or tensors (e.g., metadata.add_extra, metadata.key_shadow, tensorinfo.name_collision) automatically get correct header counts. Strategies that intentionally set wrong counts (consistency.tensor_count, consistency.metadata_count, header.tensor_count, header.metadata_kv_count, metadata.string_truncated) opt out of this sync via the CountKeeper interface.
RPC Graph-Compute Strategies¶
A separate structure-aware mutator targets the RPC_CMD_GRAPH_COMPUTE wire format — the GraphComputePayload message containing tensor descriptors and computation graph metadata. These strategies are implemented in Go (pkg/mutator/rpc/) and exported as a CGo c-archive linked into the -rpc-graph-mutator harness variant.
Category weights
The RPC mutator selects categories using weighted random sampling: Op (25%), OpParams (25%), Dimensions (20%), Graph (15%), Strides (10%), Flags (5%). Higher weights target the op dispatch and parameter passing paths where function-pointer RCE (CRUCIBLE-2026-005) and block-size assertion failures (CRUCIBLE-2026-012) were found.
Op Strategies¶
Target the ggml_op enum field that controls which computation kernel executes.
| Strategy | Description | Bug Pattern |
|---|---|---|
op.custom | Sets Op to CUSTOM/MAP_CUSTOM variants (87–90) + COMPUTE flag | Function-pointer RCE via ggml_compute_forward_custom() (005) |
op.invalid | Sets Op to out-of-range/boundary values (OP_BOUND, 0xFFFFFFFF) | GGML_ABORT in default case of ggml_compute_forward() |
OpParams Strategies¶
Target the op_params array (32 × int32) passed to computation kernels.
| Strategy | Description | Bug Pattern |
|---|---|---|
opparams.funcptr | Sets Op=CUSTOM and plants function-pointer-shaped 64-bit values in OpParams[0:1] | Direct RCE via function pointer struct layout |
opparams.random | Fills all OpParams slots with random/boundary int32 values | Bugs in op handlers reading beyond index 0 |
Dimensions Strategies¶
Target the ne[] (number of elements) array that controls tensor shape.
| Strategy | Description | Bug Pattern |
|---|---|---|
dimensions.blck_misalign | Sets NE[0] to non-multiple of block size for quantized types | Assert in ggml_row_size(), heap undersizing (012) |
dimensions.overflow | Sets NE[] entries to extreme values (0, 0xFFFFFFFF, 0x7FFFFFFF) | Size-calculation overflow in ggml_nbytes() |
dimensions.zero_dim | Sets NE[1..3] to zero | Division-by-zero (same class as 053) |
Graph Strategies¶
Target the graph structure: node count, tensor count, and source references.
| Strategy | Description | Bug Pattern |
|---|---|---|
graph.count_mismatch | Sets NNodes > NTensors (1–16 phantom nodes) | OOB memory access in graph traversal loop |
graph.large | Adds 4–32 cloned tensors with randomized op/type | Stress graph_compute() and cgraph allocation |
graph.src_chain | Wires Src[] to create dependency chains, self-references, cycles, garbage handles | Null-deref or UAF in tensor lookup during cgraph construction |
Strides Strategies¶
Target the nb[] (number of bytes) stride array.
| Strategy | Description | Bug Pattern |
|---|---|---|
strides.inconsistent | Sets NB[0] inconsistent with type's element size | Stride-based OOB access |
strides.extreme | Sets all NB[] to extreme boundary values (0, 1, 0xFFFFFFFF) | Unsigned stride arithmetic overflow |
Flags Strategies¶
Target the tensor flags bitfield.
| Strategy | Description | Bug Pattern |
|---|---|---|
flags.compute | Sets GGML_TENSOR_FLAG_COMPUTE on a random tensor | Enabler — makes ops actually execute |
flags.all | Sets flags to extreme/boundary values (0, 0xFF, 0x7FFFFFFF, -1) | Flag-gated code path bugs |
RPC Mutator Summary¶
| Category | Count | Weight | Primary Target |
|---|---|---|---|
| Op | 2 | 25% | Op dispatch / kernel selection |
| OpParams | 2 | 25% | Function pointer / parameter passing |
| Dimensions | 3 | 20% | Tensor shape / size calculations |
| Graph | 3 | 15% | Graph structure / traversal |
| Strides | 2 | 10% | Stride arithmetic / memory layout |
| Flags | 2 | 5% | Tensor flags / execution gating |
| Total | 14 | 100% |
Mutation behavior
The RPC mutator applies 1–3 random strategies per iteration. CrossOver transplants one tensor from a second input into the first at a random position. On parse failure, the mutator falls back to a minimal default (single F32 tensor) to maintain valid structure.