From fecd1848e42a24c3550878844eda302c3b61c38a Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Wed, 8 Apr 2026 18:26:57 +0000 Subject: [PATCH 1/2] smite-ir: Add `InstructionDeleteMutator` --- smite-ir-mutator/src/lib.rs | 24 ++++-- smite-ir/src/mutators.rs | 2 + smite-ir/src/mutators/instruction_delete.rs | 88 +++++++++++++++++++++ 3 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 smite-ir/src/mutators/instruction_delete.rs diff --git a/smite-ir-mutator/src/lib.rs b/smite-ir-mutator/src/lib.rs index 7eabf3fb..62125a33 100644 --- a/smite-ir-mutator/src/lib.rs +++ b/smite-ir-mutator/src/lib.rs @@ -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`], @@ -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); } @@ -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:?})", ); } diff --git a/smite-ir/src/mutators.rs b/smite-ir/src/mutators.rs index 2c076f0e..3d3d5000 100644 --- a/smite-ir/src/mutators.rs +++ b/smite-ir/src/mutators.rs @@ -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; diff --git a/smite-ir/src/mutators/instruction_delete.rs b/smite-ir/src/mutators/instruction_delete.rs new file mode 100644 index 00000000..bc31df1c --- /dev/null +++ b/smite-ir/src/mutators/instruction_delete.rs @@ -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 + } +} From 30075f5f752eb53125f62467faf17ac0125b2638 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Sun, 12 Apr 2026 07:55:25 +0000 Subject: [PATCH 2/2] smite-ir: Add tests for `InstructionDeleteMutator` --- smite-ir/src/tests.rs | 229 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 1 deletion(-) diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 0ff5f233..ca040e89 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -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. @@ -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" + ); +} + +#[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" + ); +} + +#[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; + } + } + 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); + } + } +} + // -- DeadCodeEliminator tests -- #[test]