Skip to content

Commit dc57f35

Browse files
committed
fix(decompile): detect tautological loop conditions including bitmask patterns
1 parent f086f09 commit dc57f35

1 file changed

Lines changed: 89 additions & 1 deletion

File tree

crates/vm/src/ext/exec/loop_analysis.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::core::stack::StackFrame;
22

33
/// Check if a condition is tautologically true (e.g., "arg0 == arg0", "X == (address(X))").
44
/// These create infinite loops and should be skipped as invalid loop conditions.
5+
/// Also handles bitmask patterns like "X == (X & 0xff...ff)" which are type-check equivalents.
56
pub(crate) fn is_tautologically_true_condition(condition: &str) -> bool {
67
let mut trimmed = condition.trim();
78

@@ -53,8 +54,9 @@ pub(crate) fn is_tautologically_true_condition(condition: &str) -> bool {
5354
false
5455
}
5556

56-
/// Normalize an expression for comparison by stripping type casts and parentheses.
57+
/// Normalize an expression for comparison by stripping type casts, bitmasks, and parentheses.
5758
/// For example: "address(arg0)" -> "arg0", "(arg0)" -> "arg0"
59+
/// Also handles bitmask patterns like "(arg0) & (0xff...ff)" which are equivalent to type casts.
5860
fn normalize_for_comparison(expr: &str) -> String {
5961
let mut result = expr.trim().to_string();
6062

@@ -101,9 +103,53 @@ fn normalize_for_comparison(expr: &str) -> String {
101103
}
102104
}
103105

106+
// Strip bitmask patterns like "(X) & (0xff...ff)" which are type-cast equivalents
107+
// These are used by Solidity to ensure values fit in certain types
108+
// Common masks:
109+
// - 0xffffffffffffffffffffffffffffffffffffffff (160 bits = address)
110+
// - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff (256 bits)
111+
// - 0xff (8 bits), 0xffff (16 bits), etc.
112+
if let Some(and_pos) = result.find(" & ") {
113+
let lhs = result[..and_pos].trim();
114+
let rhs = result[and_pos + 3..].trim();
115+
116+
// Check if rhs is a bitmask (all f's hex value)
117+
if is_bitmask(rhs) {
118+
// Return the normalized lhs (the actual value being masked)
119+
return normalize_for_comparison(lhs);
120+
}
121+
// Also check if lhs is the mask and rhs is the value
122+
if is_bitmask(lhs) {
123+
return normalize_for_comparison(rhs);
124+
}
125+
}
126+
104127
result
105128
}
106129

130+
/// Check if a string represents a bitmask (0xff...ff pattern)
131+
fn is_bitmask(s: &str) -> bool {
132+
let mut trimmed = s.trim();
133+
134+
// Strip parentheses
135+
while trimmed.starts_with('(') && trimmed.ends_with(')') {
136+
let inner = &trimmed[1..trimmed.len() - 1];
137+
if is_balanced_parens(inner) {
138+
trimmed = inner.trim();
139+
} else {
140+
break;
141+
}
142+
}
143+
144+
// Check for hex pattern 0xff...ff
145+
if let Some(hex_str) = trimmed.strip_prefix("0x") {
146+
// Must be non-empty and all 'f' characters (case insensitive)
147+
!hex_str.is_empty() && hex_str.chars().all(|c| c == 'f' || c == 'F')
148+
} else {
149+
false
150+
}
151+
}
152+
107153
/// Check if parentheses are balanced in a string
108154
fn is_balanced_parens(s: &str) -> bool {
109155
let mut depth = 0;
@@ -846,6 +892,48 @@ mod tests {
846892

847893
// Nested type casts
848894
assert_eq!(normalize_for_comparison("address(uint256(arg0))"), "arg0");
895+
896+
// Bitmask patterns (equivalent to type casts)
897+
assert_eq!(
898+
normalize_for_comparison("(arg1) & (0xffffffffffffffffffffffffffffffffffffffff)"),
899+
"arg1"
900+
);
901+
assert_eq!(
902+
normalize_for_comparison("((arg1) & (0xffffffffffffffffffffffffffffffffffffffff))"),
903+
"arg1"
904+
);
905+
assert_eq!(normalize_for_comparison("arg0 & 0xff"), "arg0");
906+
assert_eq!(normalize_for_comparison("(arg0) & (0xffff)"), "arg0");
907+
}
908+
909+
#[test]
910+
fn test_is_bitmask() {
911+
// Valid bitmasks
912+
assert!(is_bitmask("0xff"));
913+
assert!(is_bitmask("0xffff"));
914+
assert!(is_bitmask("0xffffffffffffffffffffffffffffffffffffffff"));
915+
assert!(is_bitmask("(0xff)"));
916+
assert!(is_bitmask("((0xffff))"));
917+
assert!(is_bitmask("0xFFFF")); // uppercase
918+
919+
// Invalid bitmasks
920+
assert!(!is_bitmask("0x01"));
921+
assert!(!is_bitmask("0xfe"));
922+
assert!(!is_bitmask("arg0"));
923+
assert!(!is_bitmask("0x"));
924+
assert!(!is_bitmask(""));
925+
}
926+
927+
#[test]
928+
fn test_is_tautologically_true_with_bitmask() {
929+
// Bitmask patterns that are tautologically true
930+
assert!(is_tautologically_true_condition(
931+
"arg1 == ((arg1) & (0xffffffffffffffffffffffffffffffffffffffff))"
932+
));
933+
assert!(is_tautologically_true_condition("arg0 == (arg0 & 0xff)"));
934+
assert!(is_tautologically_true_condition(
935+
"((arg1) & (0xffffffffffffffffffffffffffffffffffffffff)) == arg1"
936+
));
849937
}
850938

851939
#[test]

0 commit comments

Comments
 (0)