Skip to content

smite-ir/mutators: Add SpliceMutator#135

Open
Chand-ra wants to merge 4 commits into
morehouse:masterfrom
Chand-ra:splice
Open

smite-ir/mutators: Add SpliceMutator#135
Chand-ra wants to merge 4 commits into
morehouse:masterfrom
Chand-ra:splice

Conversation

@Chand-ra

Copy link
Copy Markdown

Add SpliceMutator for smite-IR. Mutates a given program by inserting a spliced input at a random point in the said program.

Comment on lines +32 to +38
for instr in &self.splice.instructions {
let shifted_inputs: Vec<usize> = instr
.inputs
.iter()
.map(|&input| input + insert_idx)
.collect();
builder.append(instr.operation.clone(), &shifted_inputs);

@Chand-ra Chand-ra Jun 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is the only logical difference between this mutator and GeneratorInsertion. It could also be implemented as:

Suggested change
for instr in &self.splice.instructions {
let shifted_inputs: Vec<usize> = instr
.inputs
.iter()
.map(|&input| input + insert_idx)
.collect();
builder.append(instr.operation.clone(), &shifted_inputs);
for instr in &self.splice.instructions {
let mut inputs = vec![];
for var_type in instr.operation.input_types() {
inputs.push(builder.pick_variable(var_type, rng));
}
builder.append(instr.operation.clone(), &inputs);
}

which I guess would "wire it stronger" to the rest of the program, but I'm not sure if that's a worthwhile tradeoff for the simplicity of the current approach.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an interesting idea, but I think the current simpler approach may be better. By doing pick_variable, we end up losing the specific values that the splice program used (which were selected as interesting after many random mutations), which probably hurts fuzzing effectiveness. We'd also need to figure out how to handle variable types for which pick_variable currently panics.

We also can get some of the same behavior already if InputSwapMutator is stacked after the splice.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd also need to figure out how to handle variable types for which pick_variable currently panics.

Would we? pick_variable() currently panics if we try to generate_fresh() a type that cannot be generated (messages, affine types, etc.).

If the corpus always has valid programs and every mutation preserves validity, won't both the input programs to SpliceMutator always be valid? That is, wouldn't every panicking type already have at least one matching candidate?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right -- we only have panic issues with generate_fresh, which shouldn't be called in this case.

We should do an experiment with the following configs to evaluate: (1) baseline, (2) this mutator, (3) a pick_variable splice mutator, (4) this mutator + a pick_variable splice mutator.

Comment thread smite-ir-mutator/src/lib.rs Outdated
Comment thread smite-ir/src/mutators/splice.rs Outdated
Comment on lines +32 to +38
for instr in &self.splice.instructions {
let shifted_inputs: Vec<usize> = instr
.inputs
.iter()
.map(|&input| input + insert_idx)
.collect();
builder.append(instr.operation.clone(), &shifted_inputs);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an interesting idea, but I think the current simpler approach may be better. By doing pick_variable, we end up losing the specific values that the splice program used (which were selected as interesting after many random mutations), which probably hurts fuzzing effectiveness. We'd also need to figure out how to handle variable types for which pick_variable currently panics.

We also can get some of the same behavior already if InputSwapMutator is stacked after the splice.

}

// Insert the spliced program.
for instr in &self.splice.instructions {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be interesting to do an experiment where we compare full-program insertion with only inserting a random subset (prefix) of the spliced program.

Full-program insertion will tend to create longer (and slower) programs, so it's possible that prefix insertion actually performs better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be interesting to do an experiment where we compare full-program insertion with only inserting a random subset (prefix) of the spliced program.

Hmm, we're inserting the prefix (instead of a random slice) so that we don't have to worry about variable dependencies. Should be easily implementable but I'm not fully sold on the "performs better" part, although I agree that it is an interesting experiment.

I think a more interesting (and perhaps advanced) version of this would be FlowInsertionMutator that selects a random Act instruction in the spliced program, yanks out its entire lineage, and inserts it at a random point in the given program.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect a prefix insertion mutator and a flow insertion mutator will perform similarly in practice, since we have the DeadCodeEliminator that will also periodically do the dependency analysis and dead instruction removal that FlowInsertionMutator would. So either one would be interesting to evaluate IMO.

It may also be interesting to combine this FlowInsertionMutator idea with the pick_variable rewiring you suggested as an experiment.

Comment thread smite-ir-mutator/src/lib.rs Outdated
Comment thread smite-ir-mutator/src/lib.rs
Comment thread smite-ir-mutator/src/lib.rs Outdated
Comment thread smite-ir/src/tests.rs Outdated

@morehouse morehouse left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM. This is ready for experimental evaluation.

Comment on lines +32 to +38
for instr in &self.splice.instructions {
let shifted_inputs: Vec<usize> = instr
.inputs
.iter()
.map(|&input| input + insert_idx)
.collect();
builder.append(instr.operation.clone(), &shifted_inputs);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right -- we only have panic issues with generate_fresh, which shouldn't be called in this case.

We should do an experiment with the following configs to evaluate: (1) baseline, (2) this mutator, (3) a pick_variable splice mutator, (4) this mutator + a pick_variable splice mutator.

Comment thread smite-ir/src/mutators/splice.rs Outdated
}

// Insert the spliced program.
for instr in &self.splice.instructions {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect a prefix insertion mutator and a flow insertion mutator will perform similarly in practice, since we have the DeadCodeEliminator that will also periodically do the dependency analysis and dead instruction removal that FlowInsertionMutator would. So either one would be interesting to evaluate IMO.

It may also be interesting to combine this FlowInsertionMutator idea with the pick_variable rewiring you suggested as an experiment.

Comment on lines +98 to 144
fn mutate_stacked(&mut self, program: &mut Program, splice: Option<Program>) {
self.last_sequence.clear();
// Power-of-two stack count: 1, 2, 4, 8, or 16 mutations.
let stack = 1u32 << self.rng.random_range(0..=4);
let splice_mutator = splice.map(SpliceMutator::new);
// Only roll up to 6 if we actually have a splice input.
let upper_bound = if splice_mutator.is_some() { 6 } else { 5 };
for _ in 0..stack {
// Uniform pick between the available mutators.
let name = match self.rng.random_range(0..5) {
let name = match self.rng.random_range(0..upper_bound) {
0 => {
OperationParamMutator.mutate(program, &mut self.rng);
"op-param"
}
1 => {
InputSwapMutator.mutate(program, &mut self.rng);
"input-swap"
}
2 => {
InstructionDeleteMutator.mutate(program, &mut self.rng);
"instr-delete"
}
3 => {
InstructionReorderMutator.mutate(program, &mut self.rng);
"instr-reorder"
}
4 => {
let generator = *AnyGenerator::ALL
.iter()
.choose(&mut self.rng)
.expect("AnyGenerator::ALL is non-empty");
let mutator = GeneratorInsertionMutator::new(generator);
mutator.mutate(program, &mut self.rng);
"gen-insert"
}
5 => {
splice_mutator
.as_ref()
.expect("splice present")
.mutate(program, &mut self.rng);
"splice"
}
_ => unreachable!("random_range() bound out of sync with match arms"),
};
self.last_sequence.push(name);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We could refactor to avoid hardcoding numbers that would need changing if we added or removed mutators.

Suggested change
/// Mutators available to [`MutatorState::mutate_stacked`]. The pool of
/// candidates is built per call, so optional entries (splice) are included by
/// pushing a variant.
#[derive(Clone, Copy)]
enum StackedMutator {
OpParam,
InputSwap,
InstrDelete,
InstrReorder,
GenInsert,
Splice,
}
fn mutate_stacked(&mut self, program: &mut Program, splice: Option<Program>) {
self.last_sequence.clear();
// Power-of-two stack count: 1, 2, 4, 8, or 16 mutations.
let stack = 1u32 << self.rng.random_range(0..=4);
let splice_mutator = splice.map(SpliceMutator::new);
let mut pool = vec![
StackedMutator::OpParam,
StackedMutator::InputSwap,
StackedMutator::InstrDelete,
StackedMutator::InstrReorder,
StackedMutator::GenInsert,
];
if splice_mutator.is_some() {
pool.push(StackedMutator::Splice);
}
for _ in 0..stack {
// Uniform pick between the available mutators.
let choice = *pool
.iter()
.choose(&mut self.rng)
.expect("pool is non-empty");
let name = match choice {
StackedMutator::OpParam => {
OperationParamMutator.mutate(program, &mut self.rng);
"op-param"
}
StackedMutator::InputSwap => {
InputSwapMutator.mutate(program, &mut self.rng);
"input-swap"
}
StackedMutator::InstrDelete => {
InstructionDeleteMutator.mutate(program, &mut self.rng);
"instr-delete"
}
StackedMutator::InstrReorder => {
InstructionReorderMutator.mutate(program, &mut self.rng);
"instr-reorder"
}
StackedMutator::GenInsert => {
let generator = *AnyGenerator::ALL
.iter()
.choose(&mut self.rng)
.expect("AnyGenerator::ALL is non-empty");
let mutator = GeneratorInsertionMutator::new(generator);
mutator.mutate(program, &mut self.rng);
"gen-insert"
}
StackedMutator::Splice => {
splice_mutator
.as_ref()
.expect("Splice is only pooled when a splice input exists")
.mutate(program, &mut self.rng);
"splice"
}
};
self.last_sequence.push(name);
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically the AnyGenerator variant for mutators. It does improve readability, but I feel like the change doesn't fit well within this PR. I'll follow up with a separate one implementing it.

Chandra Pratap added 4 commits July 7, 2026 04:43
The following commit will implement `SpliceInsertionMutator`,
which uses the decoded program from this buffer.

Additionally, add a test to ensure parsing a valid `add_buf` via
postcard doesn't panic or corrupt the state.
Refactor the common logic from `GeneratorInsertionMutator` tests
that we will need for implementing `SpliceInsertionMutator` tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants