forge/brutalizer/
assembly.rs1use 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}