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    use solar::{ast::Span, interface::BytePos};
159
160    #[test]
161    fn test_opcode_mutations_exist() {
162        let mutator = AssemblyMutator::new();
163
164        assert!(mutator.get_mutations("add").unwrap().contains(&"sub"));
165        assert!(mutator.get_mutations("mul").unwrap().contains(&"div"));
166
167        assert!(mutator.get_mutations("lt").unwrap().contains(&"gt"));
168        assert!(mutator.get_mutations("slt").unwrap().contains(&"sgt"));
169
170        assert!(mutator.get_mutations("and").unwrap().contains(&"or"));
171        assert!(mutator.get_mutations("shl").unwrap().contains(&"shr"));
172    }
173
174    #[test]
175    fn test_no_cross_family_mutations() {
176        let mutator = AssemblyMutator::new();
177        let add_alts = mutator.get_mutations("add").unwrap();
178        assert!(!add_alts.contains(&"xor"), "add should not mutate to xor (cross-family)");
179        assert!(!add_alts.contains(&"and"), "add should not mutate to and (cross-family)");
180
181        let mul_alts = mutator.get_mutations("mul").unwrap();
182        assert!(!mul_alts.contains(&"and"), "mul should not mutate to and (cross-family)");
183    }
184
185    #[test]
186    fn test_no_iszero_not_mapping() {
187        let mutator = AssemblyMutator::new();
188        assert!(mutator.get_mutations("iszero").is_none(), "iszero should not be mutated");
189        assert!(mutator.get_mutations("not").is_none(), "not should not be mutated");
190    }
191
192    #[test]
193    fn test_no_mload_sload_mapping() {
194        let mutator = AssemblyMutator::new();
195        assert!(mutator.get_mutations("mload").is_none());
196        assert!(mutator.get_mutations("sload").is_none());
197    }
198
199    #[test]
200    fn test_replace_at_span() {
201        for (case, original, outer, target, expected) in [
202            ("valid", "add(a, b)", (10, 19), (10, 13), Some("sub(a, b)")),
203            ("target outside outer", "add(a, b)", (20, 29), (10, 13), None),
204            ("target exceeds length", "add(a, b)", (10, 19), (10, 30), None),
205            ("opcode mismatch", "mul(a, b)", (10, 19), (10, 13), None),
206            ("empty original", "", (10, 19), (10, 13), None),
207        ] {
208            let outer = Span::new(BytePos(outer.0), BytePos(outer.1));
209            let target = Span::new(BytePos(target.0), BytePos(target.1));
210            assert_eq!(
211                replace_at_span(original, outer, target, "add", "sub").as_deref(),
212                expected,
213                "{case}",
214            );
215        }
216    }
217}