Skip to content

Commit 5544004

Browse files
Add per-input timeout to fuzzer to prevent infinite VM loops
1 parent 44668b2 commit 5544004

2 files changed

Lines changed: 41 additions & 33 deletions

File tree

compiler/src/bin/fuzz.rs

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ use compiler::modules::lexer::{lex, TokenType};
22
use compiler::modules::lexer::tables::token_to_str;
33
use compiler::modules::parser::{Parser, SSAChunk};
44
use compiler::modules::vm::{VM, Val};
5-
use std::{panic, time::{Duration, Instant, SystemTime}};
5+
use std::{panic, sync::mpsc, thread, time::{Duration, Instant, SystemTime}};
66

77
const MAX_LEN: usize = 2048;
88
const SAVE_DIR: &str = "crashes";
99
const PRINT_INTERVAL: u64 = 10_000;
10-
const MAX_SECS: u64 = 600; // 60 seconds
10+
const MAX_SECS: u64 = 600; // 10 minutes
11+
const VM_TIMEOUT: Duration = Duration::from_millis(200);
1112
const SLOW_THRESHOLD: Duration = Duration::from_millis(50);
1213

1314
struct Rng(u64);
@@ -275,32 +276,37 @@ impl Perf {
275276
}
276277
}
277278

278-
enum Outcome { Crash, ParseErr, VmErr, Clean(u128, Duration, Duration, Duration) }
279+
enum Outcome { Crash, ParseErr, VmErr, Timeout, Clean(u128, Duration, Duration, Duration) }
279280

280281
fn run_once(src: &str) -> Outcome {
281-
let src = if src.len() > MAX_LEN { &src[..MAX_LEN] } else { src };
282-
match panic::catch_unwind(panic::AssertUnwindSafe(|| {
283-
let t0 = Instant::now();
284-
let (tokens, _) = lex(src);
285-
let t_lex = t0.elapsed();
286-
287-
let t1 = Instant::now();
288-
let (chunk, errs) = Parser::new(src, tokens.into_iter()).parse();
289-
let t_parse = t1.elapsed();
290-
291-
let bm = opcode_bitmap(&chunk);
292-
293-
let t2 = Instant::now();
294-
let ok = VM::new(&chunk).run().is_ok();
295-
let t_vm = t2.elapsed();
296-
297-
(errs.is_empty(), ok, bm, t_lex, t_parse, t_vm)
298-
})) {
299-
Err(_) => Outcome::Crash,
300-
Ok((false, ..)) => Outcome::ParseErr,
301-
Ok((true, false, ..)) => Outcome::VmErr,
302-
Ok((true, true, bm, tl, tp, tv)) => Outcome::Clean(bm, tl, tp, tv),
303-
}
282+
let src = if src.len() > MAX_LEN { src[..MAX_LEN].to_string() } else { src.to_string() };
283+
let (tx, rx) = mpsc::channel();
284+
thread::spawn(move || {
285+
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(|| {
286+
let t0 = Instant::now();
287+
let (tokens, _) = lex(&src);
288+
let t_lex = t0.elapsed();
289+
290+
let t1 = Instant::now();
291+
let (chunk, errs) = Parser::new(&src, tokens.into_iter()).parse();
292+
let t_parse = t1.elapsed();
293+
294+
let bm = opcode_bitmap(&chunk);
295+
296+
let t2 = Instant::now();
297+
let ok = VM::new(&chunk).run().is_ok();
298+
let t_vm = t2.elapsed();
299+
300+
(errs.is_empty(), ok, bm, t_lex, t_parse, t_vm)
301+
})) {
302+
Err(_) => Outcome::Crash,
303+
Ok((false, ..)) => Outcome::ParseErr,
304+
Ok((true, false, ..)) => Outcome::VmErr,
305+
Ok((true, true, bm, tl, tp, tv)) => Outcome::Clean(bm, tl, tp, tv),
306+
};
307+
let _ = tx.send(outcome);
308+
});
309+
rx.recv_timeout(VM_TIMEOUT).unwrap_or(Outcome::Timeout)
304310
}
305311

306312
struct Corpus { entries: Vec<String>, seen: u128 }
@@ -318,14 +324,14 @@ impl Corpus {
318324
}
319325
}
320326

321-
struct Stats { iters: u64, crashes: u64, adds: u64, start: Instant }
327+
struct Stats { iters: u64, crashes: u64, adds: u64, timeouts: u64, start: Instant }
322328

323329
impl Stats {
324-
fn new() -> Self { Self { iters: 0, crashes: 0, adds: 0, start: Instant::now() } }
330+
fn new() -> Self { Self { iters: 0, crashes: 0, adds: 0, timeouts: 0, start: Instant::now() } }
325331
fn print(&self, corpus: usize, perf: &Perf) {
326332
let s = self.start.elapsed().as_secs_f64().max(0.001);
327-
eprintln!("[{:7.1}s] iters={:<9} {:.0}/s crashes={} corpus={} new_cov={}",
328-
s, self.iters, self.iters as f64 / s, self.crashes, corpus, self.adds);
333+
eprintln!("[{:7.1}s] iters={:<9} {:.0}/s crashes={} timeouts={} corpus={} new_cov={}",
334+
s, self.iters, self.iters as f64 / s, self.crashes, self.timeouts, corpus, self.adds);
329335
perf.print();
330336
}
331337
}
@@ -362,6 +368,7 @@ fn main() {
362368
}
363369
if corpus.add(input, bm) { stats.adds += 1; }
364370
}
371+
Outcome::Timeout => { stats.timeouts += 1; }
365372
_ => {}
366373
}
367374

docs/pages/implementation/fuzzing.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ corpus -> mutate -> lex + parse + vm -> catch_unwind -> [crash | new coverage |
1818
| `Crash` | panic anywhere in the pipeline | save to `crashes/crash_NNNNNN.py` |
1919
| `ParseErr` | parser emitted one or more diagnostics | discard |
2020
| `VmErr` | VM returned a typed error | discard |
21+
| `Timeout` | VM did not finish within 200 ms | discard, increment timeout counter |
2122
| `Clean(bm)` | compiled and executed without panic | admit to corpus if `bm` covers new opcodes |
2223

23-
`ParseErr` and `VmErr` are expected outcomes — typed errors are not bugs. Only an unhandled panic indicates a defect.
24+
`ParseErr`, `VmErr`, and `Timeout` are expected outcomes — typed errors and infinite loops are not bugs. Only an unhandled panic indicates a defect.
2425

2526
## Coverage
2627

@@ -30,7 +31,7 @@ An input is admitted to the corpus only when its bitmap introduces at least one
3031

3132
## Iteration
3233

33-
Some sstrategies are applied uniformly at random: `byte_flip` (XOR a random byte), `insert_keyword`, `drop_line`, `duplicate_line`, `splice` (join two corpus halves), `inject_boundary` (i64 boundary literals targeting VM overflow), `deep_nest` (100–220 bracket levels, attacks `MAX_EXPR_DEPTH`), `token_shuffle`, `indent_bomb` (50–110 nested `if True:` blocks), and `add_comment`.
34+
Some strategies are applied uniformly at random: `byte_flip` (XOR a random byte), `insert_keyword`, `drop_line`, `duplicate_line`, `splice` (join two corpus halves), `inject_boundary` (i64 boundary literals targeting VM overflow), `deep_nest` (100–220 bracket levels, attacks `MAX_EXPR_DEPTH`), `token_shuffle`, `indent_bomb` (50–110 nested `if True:` blocks), and `add_comment`.
3435

3536
## Known Targets
3637

@@ -57,7 +58,7 @@ The `fuzz` profile inherits from `release` with two overrides: `panic = "unwind"
5758
Output is written to stderr every 10 000 iterations:
5859

5960
```txt
60-
[5.3s] iters=10000 1886/s crashes=0 corpus=24 new_cov=4
61+
[5.3s] iters=10000 1886/s crashes=0 timeouts=2 corpus=24 new_cov=4
6162
```
6263

6364
Crashes are saved immediately on detection. To reproduce a crash against the standard compiler binary:

0 commit comments

Comments
 (0)