Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions smite-ir-mutator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use smite_ir::generators::{
OpenChannelGenerator,
};
use smite_ir::minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer};
use smite_ir::mutators::{InputSwapMutator, OperationParamMutator};
use smite_ir::mutators::{InputSwapMutator, InstructionDeleteMutator, OperationParamMutator};
use smite_ir::{Generator, Mutator, Program, ProgramBuilder};

/// Mutator state owned by AFL++ across calls. Allocated by [`afl_custom_init`],
Expand Down Expand Up @@ -98,12 +98,20 @@ impl MutatorState {
let stack = 1u32 << self.rng.random_range(0..=4);
for _ in 0..stack {
// Uniform pick between the available mutators.
let name = if self.rng.random() {
OperationParamMutator.mutate(program, &mut self.rng);
"op-param"
} else {
InputSwapMutator.mutate(program, &mut self.rng);
"input-swap"
let name = match self.rng.random_range(0..3) {
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"
}
_ => unreachable!("random_range() bound out of sync with match arms"),
};
self.last_sequence.push(name);
}
Expand Down Expand Up @@ -540,7 +548,7 @@ mod tests {
}
for name in suffix.split(',') {
assert!(
name == "op-param" || name == "input-swap",
name == "op-param" || name == "input-swap" || name == "instr-delete",
"unexpected mutator name in description: {name:?} (full: {s:?})",
);
}
Expand Down
2 changes: 2 additions & 0 deletions smite-ir/src/mutators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
//! structural validity. Each mutator makes a small, targeted change.

mod input_swap;
mod instruction_delete;
mod operation_param;

pub use input_swap::InputSwapMutator;
pub use instruction_delete::InstructionDeleteMutator;
pub use operation_param::OperationParamMutator;

use rand::Rng;
Expand Down
88 changes: 88 additions & 0 deletions smite-ir/src/mutators/instruction_delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Mutator that removes an instruction.

use rand::{Rng, RngExt, seq::IteratorRandom};

use super::Mutator;
use crate::Program;

/// Deletes a randomly selected instruction by removing it from
/// the instructions list and reindexing the subsequent instructions.
pub struct InstructionDeleteMutator;

impl Mutator for InstructionDeleteMutator {
fn mutate(&self, program: &mut Program, rng: &mut impl Rng) -> bool {
if program.instructions.is_empty() {
return false;
}
// Pick a random instruction to delete.
let deleted_idx = rng.random_range(0..program.instructions.len());
let deleted_type = program.instructions[deleted_idx].operation.output_type();

// Find the first instruction that uses the deleted instruction.
let first_use_idx = program
.instructions
.iter()
.position(|instr| instr.inputs.contains(&deleted_idx));

// If any instruction downstream depends on the deleted one, pick a prior
// type-matching variable to redirect those inputs to.
let replacement_idx = if let Some(first_use_idx) = first_use_idx {
let mut is_consumed = vec![false; program.instructions.len()];
if deleted_type
.expect("`None` shouldn't be consumed")
.is_affine()
{
for (i, instr) in program.instructions.iter().enumerate() {
if i == deleted_idx {
continue;
}
for &input in &instr.inputs {
if program.instructions[input].operation.output_type() == deleted_type {
is_consumed[input] = true;
}
}
}
}
match program.instructions[..first_use_idx]
.iter()
.enumerate()
.filter_map(|(i, instr)| {
(instr.operation.output_type() == deleted_type
&& i != deleted_idx
&& !is_consumed[i])
.then_some(i)
})
.choose(rng)
{
Some(idx) => {
if idx > deleted_idx {
// Deleting an instruction shifts every element past it by -1.
Some(idx - 1)
} else {
Some(idx)
}
}
// Abort if no valid replacement variable exists in the preceding scope.
None => return false,
}
} else {
None
};

// Delete from the program.
program.instructions.remove(deleted_idx);

// Heal downstream inputs: redirect references to the deleted index, and
// decrement references past it.
for instr in &mut program.instructions[deleted_idx..] {
for input in &mut instr.inputs {
if *input == deleted_idx {
*input = replacement_idx.expect("dependent input implies replacement");
} else if *input > deleted_idx {
*input -= 1;
}
}
}
true
}
}
229 changes: 228 additions & 1 deletion smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use generators::{
OpenChannelGenerator,
};
use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer};
use mutators::{InputSwapMutator, OperationParamMutator};
use mutators::{InputSwapMutator, InstructionDeleteMutator, OperationParamMutator};
use operation::{AcceptChannelField, ChannelTypeVariant, ShutdownScriptVariant};

/// Helper to build a private key with a single distinguishing byte.
Expand Down Expand Up @@ -1688,6 +1688,233 @@ fn input_swap_preserves_affine() {
}
}

// -- InstructionDeleteMutator tests --

#[test]
fn instr_delete_changes_values() {
let original = generate_open_channel_program(0);
let mut program = original.clone();
let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);

for _ in 0..100 {
mutator.mutate(&mut program, &mut rng);
}
assert_ne!(
program, original,
"InstructionDeleteMutator never changed the program"
);
}

Comment thread
morehouse marked this conversation as resolved.
#[test]
fn instr_delete_false_is_noop() {
let original = generate_open_channel_program(0);
assert_false_is_noop(&InstructionDeleteMutator, &original);
}

#[test]
fn instr_delete_returns_false_on_empty_program() {
let mut program = Program {
instructions: vec![],
};
let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);
assert!(!mutator.mutate(&mut program, &mut rng));
}

#[test]
fn instr_delete_returns_false_if_unhealable() {
let original = Program {
instructions: vec![
Instruction {
operation: Operation::LoadPrivateKey(key(1)),
inputs: vec![],
},
Instruction {
operation: Operation::DerivePoint,
inputs: vec![0],
},
],
};
let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);
let mut found_unhealable = false;

for _ in 0..100 {
let mut program = original.clone();
if !mutator.mutate(&mut program, &mut rng) {
found_unhealable = true;
break;
}
}
assert!(
found_unhealable,
"InstructionDeleteMutator never returned false for unhealable instruction"
);
}

#[test]
fn instr_delete_shifts_indices_correctly() {
let original = Program {
instructions: vec![
Instruction {
operation: Operation::LoadPrivateKey(key(1)),
inputs: vec![],
},
Instruction {
operation: Operation::LoadPrivateKey(key(2)),
inputs: vec![],
},
Instruction {
operation: Operation::DerivePoint,
inputs: vec![1, 1],
},
],
};

let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);
let mut verified_shift = false;

for _ in 0..100 {
let mut program = original.clone();
if mutator.mutate(&mut program, &mut rng) &&
program.instructions.len() == 2 &&
// Check if it deleted index 1, meaning DerivePoint is now at index 1
program.instructions[1].operation == Operation::DerivePoint
{
// input references must have shifted from [1, 1] down to [0, 0]
assert_eq!(program.instructions[1].inputs, vec![0, 0]);
verified_shift = true;
break;
}
}
assert!(
verified_shift,
"InstructionDeleteMutator never took the expected target shift path"
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be good to also test deletion of a void-output operation (i.e. SendMessage).

@Chand-ra Chand-ra May 13, 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.

I cannot say I see the merit in it, the only "interesting" thing that happens (from the mutator's perspective) on deleting a void-output operation is index-shifting of downstream instructions, which is tested here anyway. Maybe I'm missing something?

#[test]
fn instr_delete_preserves_affine_producers() {
let original = Program {
instructions: vec![
Instruction {
operation: Operation::BuildOpenChannel,
inputs: vec![],
},
Instruction {
operation: Operation::SendOpenChannel,
inputs: vec![0],
},
Instruction {
operation: Operation::RecvAcceptChannel,
inputs: vec![1],
},
Instruction {
operation: Operation::SendOpenChannel,
inputs: vec![0],
},
Instruction {
operation: Operation::RecvAcceptChannel,
inputs: vec![3],
},
],
};
let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);
let mut mutated = false;

for _ in 0..100 {
let mut program = original.clone();
// The only deletable instructions in the program are the `RecvAcceptChannel`s.
if mutator.mutate(&mut program, &mut rng) {
assert!(program.instructions.len() == original.instructions.len() - 1);
assert!(
// The first `RecvAcceptChannel` was deleted.
program.instructions[2] == original.instructions[3] ||
// The second `RecvAcceptChannel` was deleted.
program.instructions == original.instructions[0..original.instructions.len() - 1],
"InstructionDeleteMutator deleted an unhealable instruction"
);
mutated = true;
}
Comment thread
morehouse marked this conversation as resolved.
}
assert!(
mutated,
"InstructionDeleteMutator never mutated a mutable program"
);
}

#[test]
fn instr_delete_redirects_affine_consumer() {
let original = Program {
instructions: vec![
Instruction {
operation: Operation::BuildOpenChannel,
inputs: vec![],
},
Instruction {
operation: Operation::SendOpenChannel,
inputs: vec![0],
},
Instruction {
operation: Operation::LoadU16(42),
inputs: vec![],
},
Instruction {
operation: Operation::SendOpenChannel,
inputs: vec![0],
},
Instruction {
operation: Operation::RecvAcceptChannel,
inputs: vec![3],
},
],
};

let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);
let mut redirected = false;

for _ in 0..100 {
let mut program = original.clone();
if mutator.mutate(&mut program, &mut rng) {
// We are explicitly looking for the mutation where index 3 was deleted.
if program.instructions[2].operation == Operation::LoadU16(42)
&& program.instructions[3].operation == Operation::RecvAcceptChannel
{
// The orphaned RecvAcceptChannel must have been redirected to the
// prior unconsumed affine variable at index 1.
assert_eq!(
program.instructions[3].inputs,
vec![1],
"Affine consumer was not redirected to the unconsumed affine variable"
);
redirected = true;
}
}
}
assert!(
redirected,
"InstructionDeleteMutator never triggered the affine redirection path"
);
}

#[test]
fn instr_delete_maintains_validity() {
let original = generate_open_channel_program(0);
let mutator = InstructionDeleteMutator;
let mut rng = SmallRng::seed_from_u64(0);

for _ in 0..100 {
let mut program = original.clone();
if mutator.mutate(&mut program, &mut rng) {
assert_well_formed(&program);
}
Comment thread
morehouse marked this conversation as resolved.
}
}

// -- DeadCodeEliminator tests --

#[test]
Expand Down