Skip to main content

panic_hunt/
panic_hunt.rs

1use std::{cell::RefCell, collections::BTreeMap, panic};
2
3use ps_parser::PowerShellSession;
4
5thread_local! {
6    static LAST: RefCell<String> = RefCell::new(String::new());
7}
8
9const TARGETED: &[&str] = &[
10    "9223372036854775807 + 1",
11    "-9223372036854775807 - 10",
12    "9223372036854775807 * 2",
13    "$a = 9223372036854775807; $a++",
14    "[long]::MinValue / -1",
15    "(-9223372036854775807 - 1) / -1",
16    "(-9223372036854775807 - 1) % -1",
17    "5 / '0.4'",
18    "5 % '0.4'",
19    "5 / 0.4",
20    "5 % 0.4",
21    "1 -shl 64",
22    "1 -shl -1",
23    "1 -shr 100",
24    "'zażółć'.Substring(3)",
25    "'zażółć'.Substring(2,2)",
26    "'zażółć'.Remove(3)",
27    "'abc'.Substring(-1)",
28    "'abc'.Substring(1,-1)",
29    "'abc'.Remove(-1)",
30    "'abc'.Substring(9223372036854775807, 1)",
31    "'zażółć'.PadLeft(10)",
32    "'zażółć'.Trim('ż')",
33    "'zażółć'.ToUpper()",
34    "'zażółć'.IndexOf('ó')",
35    "'zażółć'[3]",
36    "'abc'[-10]",
37    "@(1,2,3)[-10]",
38    "@(1,2,3)[9223372036854775807]",
39    "@()[0]",
40    "'abc'[1..-1]",
41    "'zażółć' -split 'ó'",
42    "'zażółć' -replace 'ó','o'",
43    "'a' -split ''",
44    "'{0}{1}' -f 1",
45    "'{99999999999}' -f 1",
46    "'{0' -f 1",
47    "'{' -f 1",
48    "[char]-1",
49    "[char]99999999",
50    "[int]'99999999999999999999'",
51    "[int]9223372036854775807",
52    "[byte]300",
53    "[System.Text.Encoding]::Unicode.GetString([byte[]](1,2,3))",
54    "[System.Text.Encoding]::UTF8.GetString([byte[]](255,254))",
55    "[Convert]::FromBase64String('abc')",
56    "[Convert]::FromBase64String('====')",
57    "[System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('YQ=='))",
58    "powershell -enc",
59    "powershell -enc ''",
60    "powershell -enc YQ==",
61    "powershell -e '!!!'",
62    "powershell -encodedcommand -enc",
63    "& {}",
64    "& {param($a)} ",
65    "function f {} ; f 1 2 3",
66    "$null.foo.bar()",
67    "$null[0]",
68    "$x = @{}; $x[$null]",
69    "switch ($null) {}",
70    "switch (1) { default {} default {} }",
71    "1..-1",
72    "'a'..'z'",
73    "[char]'a'..[char]'c'",
74    "\"$(\"",
75    "\"$(1\"",
76    "'''",
77    "@'\n'@",
78    "@\"\n\"@",
79    "$",
80    "${",
81    "${}",
82    "$env:",
83    "-",
84    "--",
85    "!",
86    "[",
87    "[]",
88    "[int]",
89    "[int]::",
90    "::",
91    "()",
92    "@(",
93    "@{",
94    "@{a=}",
95    "{",
96    "}",
97    "|",
98    "| x",
99    "1 |",
100    "a.b.c(",
101    ".",
102    "&",
103    "&&",
104    "`",
105    "#",
106    "<#",
107    "class A { }",
108    "class A { [int]$x; A() { } }",
109    "class",
110    "function",
111    "if",
112    "if ()",
113    "while",
114    "for (;;) { break }",
115    "foreach",
116    "try { } catch",
117    "1 -as [int]",
118    "1 -is",
119    "'a' -like",
120    "'a' -match '('",
121    "'a' -replace '(', 'b'",
122    "'a' -split '('",
123    "'a' -match '\\'",
124    "-join",
125    "-join $null",
126    "1 -bxor",
127    "-not",
128    "[System.Convert]::ToInt32('ff', 16)",
129    "[System.Convert]::ToInt32('ff', 0)",
130    "[System.Convert]::ToInt32('zz', 16)",
131    "[System.Convert]::ToString(5, 0)",
132    "[string]::new('a', -1)",
133    "[string]::Join()",
134    "'abc'.Split()",
135    "'abc'.Insert(10, 'x')",
136    "'abc'.Replace('', 'x')",
137    "'abc'.LastIndexOf('c', 100)",
138    "'abc'.IndexOf('c', 100)",
139    "'{0:N70000}' -f 1.5",
140    "'{0:F70000}' -f 1.5",
141    "'{0:N}' -f 1.5",
142    "'{0:}' -f 1.5",
143    "'{0,70000}' -f 1",
144    "'{0,-2147483648}' -f 1",
145    "'{0,2147483647}' -f 1",
146    "'{0:#,0}' -f 1",
147    "'{ż}' -f 1",
148    "'{0,ż}' -f 1",
149    "'{0:ż}' -f 1",
150    "'a'.Split('a', -9223372036854775807 - 1)",
151    "'a'.Split('a', -5)",
152    "@(1,2) * 0",
153    "'a' * 0",
154    "class B : A { }",
155    "class A { [int]$x; A() { } }; [A]::new()",
156    "'{0:' + '0' * 70000 + '}' -f 1",
157];
158
159// each case on a 1 MB stack (Windows main thread size), with a timeout; a stack overflow aborts the whole run
160fn depth_matrix() {
161    let patterns: &[(&str, &str, &str)] = &[
162        ("paren", "(", ")"),
163        ("block", "& {", "}"),
164        ("sub", "$(", ")"),
165        ("dq", "\"$(", ")\""),
166        ("idx", "$a[", "]"),
167        ("if", "if(1){", "}"),
168        ("ifelse", "if(0){1}else{", "}"),
169        ("arr", "@(", ")"),
170        ("hash", "@{a=", "}"),
171        ("neg", "-(", ")"),
172        ("method", "'a'.Replace((", "),'b')"),
173        ("plus", "+(", ")"),
174        ("not", "!(", ")"),
175        ("cast", "[int](", ")"),
176        ("sb", "{", "}"),
177        ("cmd", "echo (", ")"),
178    ];
179    let only = std::env::args().nth(2);
180    let depths: Vec<usize> = match std::env::args().nth(3) {
181        Some(d) => d.split(',').map(|d| d.parse().unwrap()).collect(),
182        None => vec![5, 10, 20, 40, 60, 80, 100, 300, 1500, 5000],
183    };
184    for n in depths {
185        println!("--- depth {n}");
186        let mut cases: Vec<(String, String)> = patterns
187            .iter()
188            .filter(|(name, _, _)| only.as_deref().is_none_or(|o| o == "all" || o == *name))
189            .map(|(name, o, c)| (name.to_string(), format!("$a=1;{}1{}", o.repeat(n), c.repeat(n))))
190            .collect();
191        cases.push((
192            "func".into(),
193            format!("function f($n) {{ if ($n -gt 0) {{ f ($n - 1) }} else {{ 'done' }} }}; f {n}"),
194        ));
195        for (name, script) in cases {
196            let (tx, rx) = std::sync::mpsc::channel();
197            std::thread::Builder::new()
198                .stack_size(1 << 20)
199                .spawn(move || {
200                    let t = std::time::Instant::now();
201                    let r = PowerShellSession::new().parse_script(&script).map(|r| r.result());
202                    let _ = tx.send(format!("{:?} [{} ms]", r, t.elapsed().as_millis()));
203                })
204                .unwrap();
205            let res = rx
206                .recv_timeout(std::time::Duration::from_secs(20))
207                .unwrap_or_else(|_| "TIMEOUT".into());
208            let short: String = res.chars().take(70).collect();
209            println!("{name:>7}: {short}");
210        }
211    }
212    std::process::exit(0);
213}
214
215fn run(script: &str) -> Option<String> {
216    LAST.with(|l| l.borrow_mut().clear());
217    let res = panic::catch_unwind(|| {
218        let mut ps = PowerShellSession::new();
219        let _ = ps.parse_script(script);
220        let _ = PowerShellSession::new().parse_command(script);
221    });
222    // the hook fires even when the parser catches the panic internally
223    let last = LAST.with(|l| l.borrow().clone());
224    (res.is_err() || !last.is_empty()).then_some(last)
225}
226
227fn main() {
228    panic::set_hook(Box::new(|info| {
229        let loc = info
230            .location()
231            .map(|l| format!("{}:{}", l.file(), l.line()))
232            .unwrap_or_default();
233        let msg = info
234            .payload()
235            .downcast_ref::<&str>()
236            .map(|s| s.to_string())
237            .or_else(|| info.payload().downcast_ref::<String>().cloned())
238            .unwrap_or_default();
239        LAST.with(|l| *l.borrow_mut() = format!("{loc} | {msg}"));
240    }));
241
242    if std::env::args().nth(1).as_deref() == Some("--depth") {
243        depth_matrix();
244        return;
245    }
246
247    if let Some(script) = std::env::args().nth(1) {
248        let (tx, rx) = std::sync::mpsc::channel();
249        std::thread::spawn(move || {
250            let t = std::time::Instant::now();
251            let r = PowerShellSession::new().parse_script(&script).map(|r| r.result());
252            let _ = tx.send(format!("[{} ms] {:?}", t.elapsed().as_millis(), r));
253        });
254        let res = rx
255            .recv_timeout(std::time::Duration::from_secs(15))
256            .unwrap_or_else(|_| "TIMEOUT".into());
257        println!("{}", res.chars().take(200).collect::<String>());
258        std::process::exit(0);
259    }
260
261    // location -> (count, shortest input)
262    let mut found: BTreeMap<String, (usize, String)> = BTreeMap::new();
263    let mut record = |input: &str, panic: String| {
264        let key = panic.split(" | ").next().unwrap_or_default().to_string();
265        let e = found.entry(key).or_insert((0, input.to_string()));
266        e.0 += 1;
267        if input.len() < e.1.len() {
268            e.1 = input.to_string();
269        }
270        let _ = panic;
271    };
272
273    let mut runs = 0;
274    for s in TARGETED {
275        runs += 1;
276        if let Some(p) = run(s) {
277            println!("TARGETED PANIC: {s:?}\n    {p}");
278            record(s, p);
279        }
280    }
281
282    let corpus = [
283        "test_scripts/comprehensive/input.ps1",
284        "test_scripts/parser_focused/input.ps1",
285        "test_scripts/stress/input.ps1",
286        "test_scripts/enc_oneliner.ps1",
287    ];
288    for path in corpus {
289        // local samples (e.g. enc_oneliner.ps1) are not in the repo
290        let Ok(text) = std::fs::read_to_string(path) else {
291            continue;
292        };
293        let lines: Vec<&str> = text.lines().collect();
294
295        // every single line on its own, and every line truncated at each char boundary
296        for line in &lines {
297            let idxs: Vec<usize> = line.char_indices().map(|(i, _)| i).chain([line.len()]).collect();
298            for &i in &idxs {
299                runs += 1;
300                let s = &line[..i];
301                if let Some(p) = run(s) {
302                    record(s, p);
303                }
304            }
305        }
306
307        // whole file truncated at every line boundary
308        for n in 0..=lines.len() {
309            runs += 1;
310            let s = lines[..n].join("\n");
311            if let Some(p) = run(&s) {
312                record(&s, p);
313            }
314        }
315    }
316
317    println!("\n=== {runs} runs, {} distinct panic locations ===", found.len());
318    for (loc, (count, input)) in &found {
319        let short: String = input.chars().take(160).collect();
320        println!("{loc}  (x{count})\n    shortest: {short:?}");
321    }
322}