Skip to main content

forge/mutation/mutators/
assembly_mutator.rs

1use std::collections::HashMap;
2
3use eyre::Result;
4use solar::ast::yul;
5
6use super::{MutationContext, Mutator};
7use crate::mutation::mutant::{Mutant, MutationType};
8
9pub struct AssemblyMutator {
10    opcode_mutations: HashMap<&'static str, Vec<&'static str>>,
11}
12
13impl Default for AssemblyMutator {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl AssemblyMutator {
20    pub fn new() -> Self {
21        let mut opcode_mutations: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
22
23        // Arithmetic — stay within arithmetic family
24        opcode_mutations.insert("add", vec!["sub", "mul"]);
25        opcode_mutations.insert("sub", vec!["add", "mul", "div"]);
26        opcode_mutations.insert("mul", vec!["add", "div"]);
27        opcode_mutations.insert("div", vec!["mul", "sub", "mod"]);
28        opcode_mutations.insert("sdiv", vec!["smod", "mul"]);
29        opcode_mutations.insert("mod", vec!["div", "mul"]);
30        opcode_mutations.insert("smod", vec!["sdiv", "mod"]);
31        opcode_mutations.insert("exp", vec!["mul", "add"]);
32        opcode_mutations.insert("addmod", vec!["mulmod"]);
33        opcode_mutations.insert("mulmod", vec!["addmod"]);
34
35        // Comparisons — stay within comparison family
36        opcode_mutations.insert("lt", vec!["gt", "eq", "slt"]);
37        opcode_mutations.insert("gt", vec!["lt", "eq", "sgt"]);
38        opcode_mutations.insert("slt", vec!["sgt", "lt"]);
39        opcode_mutations.insert("sgt", vec!["slt", "gt"]);
40        opcode_mutations.insert("eq", vec!["lt", "gt"]);
41
42        // Bitwise — stay within bitwise family
43        opcode_mutations.insert("and", vec!["or", "xor"]);
44        opcode_mutations.insert("or", vec!["and", "xor"]);
45        opcode_mutations.insert("xor", vec!["and", "or"]);
46
47        // Shifts — stay within shift family
48        opcode_mutations.insert("shl", vec!["shr", "sar"]);
49        opcode_mutations.insert("shr", vec!["shl", "sar"]);
50        opcode_mutations.insert("sar", vec!["shr", "shl"]);
51
52        Self { opcode_mutations }
53    }
54
55    pub fn get_mutations(&self, opcode: &str) -> Option<&[&'static str]> {
56        self.opcode_mutations.get(opcode).map(|v| v.as_slice())
57    }
58}
59
60impl Mutator for AssemblyMutator {
61    fn generate_mutants(&self, context: &MutationContext<'_>) -> Result<Vec<Mutant>> {
62        let yul_expr = context.yul_expr.ok_or_else(|| eyre::eyre!("No Yul expression"))?;
63
64        let call = match &yul_expr.kind {
65            yul::ExprKind::Call(call) => call,
66            _ => return Ok(vec![]),
67        };
68
69        let opcode_name = call.name.as_str();
70
71        let alternatives = match self.get_mutations(opcode_name) {
72            Some(alts) => alts,
73            None => return Ok(vec![]),
74        };
75
76        let original = context.original_text();
77        if original.is_empty() {
78            return Ok(vec![]);
79        }
80
81        let expected_len = (context.span.hi().0 - context.span.lo().0) as usize;
82        if original.len() != expected_len {
83            return Ok(vec![]);
84        }
85
86        let source_line = context.source_line();
87        let line_number = context.line_number();
88        let column_number = context.column_number();
89
90        let name_span = call.name.span;
91
92        let mutants = alternatives
93            .iter()
94            .filter_map(|&new_opcode| {
95                let mutated =
96                    replace_at_span(&original, context.span, name_span, opcode_name, new_opcode)?;
97                Some(Mutant {
98                    span: context.span,
99                    mutation: MutationType::YulOpcode {
100                        original_opcode: opcode_name.to_string(),
101                        new_opcode: new_opcode.to_string(),
102                        mutated_expr: mutated,
103                    },
104                    path: context.path.clone(),
105                    original: original.clone(),
106                    source_line: source_line.clone(),
107                    line_number,
108                    column_number,
109                })
110            })
111            .collect();
112
113        Ok(mutants)
114    }
115
116    fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
117        if let Some(yul_expr) = ctxt.yul_expr
118            && let yul::ExprKind::Call(call) = &yul_expr.kind
119        {
120            return self.opcode_mutations.contains_key(call.name.as_str());
121        }
122        false
123    }
124}
125
126fn replace_at_span(
127    original: &str,
128    outer_span: solar::ast::Span,
129    target_span: solar::ast::Span,
130    expected_opcode: &str,
131    replacement: &str,
132) -> Option<String> {
133    let outer_lo = outer_span.lo().0 as usize;
134    let target_lo = target_span.lo().0 as usize;
135    let target_hi = target_span.hi().0 as usize;
136
137    let rel_lo = target_lo.checked_sub(outer_lo)?;
138    let rel_hi = target_hi.checked_sub(outer_lo)?;
139
140    if rel_lo > rel_hi || rel_hi > original.len() {
141        return None;
142    }
143
144    let prefix = original.get(..rel_lo)?;
145    let replaced = original.get(rel_lo..rel_hi)?;
146    let suffix = original.get(rel_hi..)?;
147
148    if replaced != expected_opcode {
149        return None;
150    }
151
152    Some(format!("{prefix}{replacement}{suffix}"))
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_opcode_mutations_exist() {
161        let mutator = AssemblyMutator::new();
162
163        assert!(mutator.get_mutations("add").unwrap().contains(&"sub"));
164        assert!(mutator.get_mutations("mul").unwrap().contains(&"div"));
165
166        assert!(mutator.get_mutations("lt").unwrap().contains(&"gt"));
167        assert!(mutator.get_mutations("slt").unwrap().contains(&"sgt"));
168
169        assert!(mutator.get_mutations("and").unwrap().contains(&"or"));
170        assert!(mutator.get_mutations("shl").unwrap().contains(&"shr"));
171    }
172
173    #[test]
174    fn test_no_cross_family_mutations() {
175        let mutator = AssemblyMutator::new();
176        let add_alts = mutator.get_mutations("add").unwrap();
177        assert!(!add_alts.contains(&"xor"), "add should not mutate to xor (cross-family)");
178        assert!(!add_alts.contains(&"and"), "add should not mutate to and (cross-family)");
179
180        let mul_alts = mutator.get_mutations("mul").unwrap();
181        assert!(!mul_alts.contains(&"and"), "mul should not mutate to and (cross-family)");
182    }
183
184    #[test]
185    fn test_no_iszero_not_mapping() {
186        let mutator = AssemblyMutator::new();
187        assert!(mutator.get_mutations("iszero").is_none(), "iszero should not be mutated");
188        assert!(mutator.get_mutations("not").is_none(), "not should not be mutated");
189    }
190
191    #[test]
192    fn test_no_mload_sload_mapping() {
193        let mutator = AssemblyMutator::new();
194        assert!(mutator.get_mutations("mload").is_none());
195        assert!(mutator.get_mutations("sload").is_none());
196    }
197
198    #[test]
199    fn test_replace_at_span_valid() {
200        use solar::interface::BytePos;
201        let original = "add(a, b)";
202        let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
203        let target = solar::ast::Span::new(BytePos(10), BytePos(13));
204        let result = replace_at_span(original, outer, target, "add", "sub");
205        assert_eq!(result, Some("sub(a, b)".to_string()));
206    }
207
208    #[test]
209    fn test_replace_at_span_target_outside_outer() {
210        use solar::interface::BytePos;
211        let original = "add(a, b)";
212        let outer = solar::ast::Span::new(BytePos(20), BytePos(29));
213        let target = solar::ast::Span::new(BytePos(10), BytePos(13));
214        assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
215    }
216
217    #[test]
218    fn test_replace_at_span_target_exceeds_length() {
219        use solar::interface::BytePos;
220        let original = "add(a, b)";
221        let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
222        let target = solar::ast::Span::new(BytePos(10), BytePos(30));
223        assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
224    }
225
226    #[test]
227    fn test_replace_at_span_opcode_mismatch() {
228        use solar::interface::BytePos;
229        let original = "mul(a, b)";
230        let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
231        let target = solar::ast::Span::new(BytePos(10), BytePos(13));
232        assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
233    }
234
235    #[test]
236    fn test_replace_at_span_empty_original() {
237        use solar::interface::BytePos;
238        let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
239        let target = solar::ast::Span::new(BytePos(10), BytePos(13));
240        assert!(replace_at_span("", outer, target, "add", "sub").is_none());
241    }
242}