Skip to main content

forge/brutalizer/
assembly.rs

1use solar::{
2    ast::{Block, FunctionKind, ItemFunction, Span, StmtKind, Visibility},
3    interface::BytePos,
4};
5
6use super::{span_seed, splitmix64, transform::Transform};
7
8pub(super) fn assembly_transforms(func: &ItemFunction<'_>) -> Vec<Transform> {
9    let Some(body) = &func.body else { return Vec::new() };
10
11    let visibility = func.header.visibility();
12    let kind = Some(func.kind);
13    if !block_contains_assembly(body) || !is_eligible_function(visibility, kind) {
14        return Vec::new();
15    }
16
17    let insert_pos = body.span.lo().0 + 1;
18    let insert_span = Span::new(BytePos(insert_pos), BytePos(insert_pos));
19    let replacement = format!(
20        "{}{}",
21        generate_memory_brutalization_assembly(insert_span),
22        generate_fmp_misalignment_assembly(insert_span)
23    );
24    vec![Transform::Insert { offset: insert_pos as usize, replacement }]
25}
26
27const fn is_eligible_function(visibility: Option<Visibility>, kind: Option<FunctionKind>) -> bool {
28    if let Some(kind) = kind
29        && !matches!(kind, FunctionKind::Function)
30    {
31        return false;
32    }
33
34    matches!(visibility, Some(Visibility::External))
35}
36
37fn generate_memory_brutalization_assembly(span: Span) -> String {
38    let s = span_seed(span);
39    let w0 = splitmix64(s);
40    let w1 = splitmix64(s.wrapping_add(1));
41    let w2 = splitmix64(s.wrapping_add(2));
42    let w3 = splitmix64(s.wrapping_add(3));
43    let w4 = splitmix64(s.wrapping_add(4));
44    let w5 = splitmix64(s.wrapping_add(5));
45    let w6 = splitmix64(s.wrapping_add(6));
46    let w7 = splitmix64(s.wrapping_add(7));
47    let s0 = splitmix64(s.wrapping_add(8));
48    let s1 = splitmix64(s.wrapping_add(9));
49    let s2 = splitmix64(s.wrapping_add(10));
50    let s3 = splitmix64(s.wrapping_add(11));
51    format!(
52        " assembly {{ \
53        mstore(0x00, 0x{w0:016x}{w1:016x}{w2:016x}{w3:016x}) \
54        mstore(0x20, 0x{w4:016x}{w5:016x}{w6:016x}{w7:016x}) \
55        let _b_p := mload(0x40) \
56        mstore(_b_p, 0x{s0:016x}{s1:016x}{s2:016x}{s3:016x}) \
57        for {{ let _b_i := 0x20 }} lt(_b_i, 0x400) {{ _b_i := add(_b_i, 0x20) }} {{ \
58        mstore(add(_b_p, _b_i), keccak256(add(_b_p, sub(_b_i, 0x20)), 0x20)) \
59        }} \
60        }} "
61    )
62}
63
64fn generate_fmp_misalignment_assembly(span: Span) -> String {
65    let offset = deterministic_fmp_offset(span);
66    format!(" assembly {{ mstore(0x40, add(mload(0x40), {offset})) }} ")
67}
68
69fn deterministic_fmp_offset(span: Span) -> u8 {
70    ((span_seed(span) % 31) as u8) | 1
71}
72
73fn block_contains_assembly(block: &Block<'_>) -> bool {
74    block.stmts.iter().any(|stmt| stmt_contains_assembly(&stmt.kind))
75}
76
77fn stmt_contains_assembly(kind: &StmtKind<'_>) -> bool {
78    match kind {
79        StmtKind::Assembly(_) => true,
80        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => block_contains_assembly(block),
81        StmtKind::If(_, then_stmt, else_stmt) => {
82            stmt_contains_assembly(&then_stmt.kind)
83                || else_stmt.as_ref().is_some_and(|s| stmt_contains_assembly(&s.kind))
84        }
85        StmtKind::While(_, body) | StmtKind::DoWhile(body, _) => stmt_contains_assembly(&body.kind),
86        StmtKind::For { body, .. } => stmt_contains_assembly(&body.kind),
87        StmtKind::Try(try_stmt) => {
88            try_stmt.clauses.iter().any(|clause| block_contains_assembly(&clause.block))
89        }
90        _ => false,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::path::Path;
97
98    use super::deterministic_fmp_offset;
99    use crate::brutalizer::brutalize_source;
100    use solar::{ast::Span, interface::BytePos};
101
102    fn brutalize(source: &str) -> String {
103        brutalize_source(Path::new("test.sol"), source).unwrap()
104    }
105
106    #[test]
107    fn memory_injected_for_external_assembly_function() {
108        let source = r#"
109pragma solidity ^0.8.0;
110contract T {
111    function f() external pure returns (uint256 r) {
112        assembly { r := 42 }
113    }
114}
115"#;
116        let result = brutalize(source);
117        assert!(result.contains("mstore(0x00,"));
118        assert!(result.contains("mstore(0x20,"));
119        assert!(result.contains("let _b_p := mload(0x40)"));
120        assert!(result.contains("keccak256("));
121    }
122
123    #[test]
124    fn fmp_injected_for_external_assembly_function() {
125        let source = r#"
126pragma solidity ^0.8.0;
127contract T {
128    function f() external pure returns (uint256 r) {
129        assembly { r := 42 }
130    }
131}
132"#;
133        let result = brutalize(source);
134        assert!(result.contains("mstore(0x40, add(mload(0x40),"));
135    }
136
137    #[test]
138    fn memory_fill_precedes_fmp_misalignment() {
139        let source = r#"
140pragma solidity ^0.8.0;
141contract T {
142    function f() external pure returns (uint256 r) {
143        assembly { r := 42 }
144    }
145}
146"#;
147        let result = brutalize(source);
148        let memory_pos = result.find("let _b_p := mload(0x40)").unwrap();
149        let fmp_pos = result.find("mstore(0x40, add(mload(0x40),").unwrap();
150        assert!(memory_pos < fmp_pos);
151    }
152
153    #[test]
154    fn not_injected_for_non_assembly_function() {
155        let source = r#"
156pragma solidity ^0.8.0;
157contract T {
158    function f() external pure returns (uint256) {
159        return 42;
160    }
161}
162"#;
163        let result = brutalize(source);
164        assert!(!result.contains("mstore(0x00,"));
165        assert!(!result.contains("mstore(0x40, add("));
166    }
167
168    #[test]
169    fn not_injected_for_public_function() {
170        let source = r#"
171pragma solidity ^0.8.0;
172contract T {
173    function f() public pure returns (uint256 r) {
174        assembly { r := 42 }
175    }
176}
177"#;
178        let result = brutalize(source);
179        assert!(!result.contains("mstore(0x00,"));
180    }
181
182    #[test]
183    fn not_injected_for_internal_function() {
184        let source = r#"
185pragma solidity ^0.8.0;
186contract T {
187    function f() internal pure returns (uint256 r) {
188        assembly { r := 42 }
189    }
190}
191"#;
192        let result = brutalize(source);
193        assert!(!result.contains("mstore(0x00,"));
194    }
195
196    #[test]
197    fn fmp_offset_is_odd_and_bounded() {
198        for i in 0..100u32 {
199            let span = Span::new(BytePos(i), BytePos(i + 10));
200            let offset = deterministic_fmp_offset(span);
201            assert!(offset > 0, "offset must be non-zero");
202            assert!(offset < 32, "offset must be < 32");
203            assert!(offset % 2 == 1, "offset must be odd");
204        }
205    }
206}