1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use std::collections::HashSet;
use super::super::super::HighLevelEmitter;
impl HighLevelEmitter {
/// Recognises the `label_X: ... goto label_X;` shape with no other
/// references to `label_X` and rewrites it to a `loop { ... }` block.
/// This is the canonical pattern for an unconditional infinite loop
/// produced by the Neo C# compiler; lifting it removes both the label
/// and the goto, leaving idiomatic source.
pub(crate) fn rewrite_label_goto_to_loop(statements: &mut [String]) {
// A `label_X:` can only fold into a `loop { }` when a matching STANDALONE
// `goto label_X;` exists. Pre-collect those lines once so labels without
// one are skipped in O(1) instead of each scanning the whole tail of the
// vector. Without this the pass is O(labels x N): a crafted in-cap NEF
// that emits many guarded gotos (e.g. crossing JMPIF chains, whose gotos
// render as inline `if c { goto X; }` and never match the standalone
// form) drives it quadratic — a decompiler-hang DoS.
let standalone_gotos: HashSet<String> = statements
.iter()
.map(|stmt| stmt.trim())
.filter(|trimmed| trimmed.starts_with("goto label_") && trimmed.ends_with(';'))
.map(str::to_string)
.collect();
if standalone_gotos.is_empty() {
return;
}
let mut index = 0;
while index < statements.len() {
let trimmed = statements[index].trim().to_string();
// Match: `label_0xXXXX:`
let Some(label) = trimmed.strip_suffix(':') else {
index += 1;
continue;
};
if !label.starts_with("label_") {
index += 1;
continue;
}
let label = label.to_string();
// Find the matching `goto label_X;` at the same brace depth.
let goto_target = format!("goto {label};");
// Fast-skip: with no standalone goto for this label, the forward
// search below can only fail, so skip the scan entirely.
if !standalone_gotos.contains(&goto_target) {
index += 1;
continue;
}
let mut depth = 0i32;
let mut goto_idx = None;
for (j, stmt) in statements.iter().enumerate().skip(index + 1) {
let t = stmt.trim();
if t == goto_target && depth == 0 {
goto_idx = Some(j);
break;
}
if t.ends_with('{') {
depth += 1;
}
if t == "}" || t.starts_with("} ") {
depth -= 1;
if depth < 0 {
// Exited the enclosing block before finding the goto.
break;
}
}
}
let Some(goto_idx) = goto_idx else {
index += 1;
continue;
};
// Bail if any other reference to the label appears anywhere —
// a second goto means the label is a structured-jump target,
// not just a back-edge for an infinite loop, and lifting would
// change semantics. (`label_decl` is hoisted out of the scan so it
// is allocated once rather than per statement, and `.any()`
// short-circuits on the first extra reference.)
let label_decl = format!("{label}:");
let has_other_reference = statements.iter().enumerate().any(|(i, stmt)| {
i != index
&& i != goto_idx
&& (stmt.trim() == label_decl || stmt.contains(&goto_target))
});
if has_other_reference {
index += 1;
continue;
}
// Preserve indentation from the original label line so the
// loop block stays aligned with surrounding code.
let label_indent_len = statements[index].len() - statements[index].trim_start().len();
let label_indent = statements[index][..label_indent_len].to_string();
let goto_indent_len =
statements[goto_idx].len() - statements[goto_idx].trim_start().len();
let goto_indent = statements[goto_idx][..goto_indent_len].to_string();
statements[index] = format!("{label_indent}loop {{");
statements[goto_idx] = format!("{goto_indent}}}");
index += 1;
}
}
}