Skip to content

Commit 737b687

Browse files
fix(fuzz): prevent abort on stack overflow and OOM
1 parent 04472a5 commit 737b687

5 files changed

Lines changed: 27 additions & 4 deletions

File tree

compiler/src/bin/fuzz.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ const BOUNDARIES: [i64; 13] = [
9090

9191
fn boundary_int(rng: &mut Rng) -> i64 { BOUNDARIES[rng.usize_in(BOUNDARIES.len())] }
9292

93+
/* 25% boundary values; rest are full-range random i64 */
9394
fn rand_int(rng: &mut Rng) -> String {
9495
if rng.usize_in(4) == 0 { boundary_int(rng).to_string() }
9596
else { (rng.next() as i64).to_string() }
9697
}
9798

99+
/* Picks one of ten mutation strategies at uniform random */
98100
fn mutate(src: &str, corpus: &[String], rng: &mut Rng) -> String {
99101
match rng.usize_in(10) {
100102
0 => byte_flip(src, rng),
@@ -119,12 +121,14 @@ fn byte_flip(src: &str, rng: &mut Rng) -> String {
119121
String::from_utf8_lossy(&bytes).into_owned()
120122
}
121123

124+
/* Splits into lines, applies f in place, rejoins; shared by drop/duplicate */
122125
fn with_lines(src: &str, f: impl FnOnce(&mut Vec<&str>)) -> String {
123126
let mut lines: Vec<&str> = src.lines().collect();
124127
f(&mut lines);
125128
lines.join("\n")
126129
}
127130

131+
/* Injects a keyword snippet at a random line; exercises keywords in unexpected positions */
128132
fn insert_keyword(src: &str, rng: &mut Rng) -> String {
129133
let kw = rand_keyword(rng);
130134
let name = rand_name(rng);
@@ -153,6 +157,7 @@ fn duplicate_line(src: &str, rng: &mut Rng) -> String {
153157
with_lines(src, |lines| { let idx = rng.usize_in(lines.len()); lines.insert(idx, lines[idx]); })
154158
}
155159

160+
/* Cross-seeds two corpus entries to produce novel program shapes */
156161
fn splice(src: &str, corpus: &[String], rng: &mut Rng) -> String {
157162
if corpus.is_empty() { return src.to_string(); }
158163
let other = &corpus[rng.usize_in(corpus.len())];
@@ -165,6 +170,7 @@ fn splice(src: &str, corpus: &[String], rng: &mut Rng) -> String {
165170
out.join("\n")
166171
}
167172

173+
/* Replaces the first numeric literal with a NaN-box boundary value */
168174
fn inject_boundary(src: &str, rng: &mut Rng) -> String {
169175
let boundary = boundary_int(rng).to_string();
170176
let bytes = src.as_bytes();
@@ -220,6 +226,7 @@ fn indent_bomb(rng: &mut Rng) -> String {
220226
out
221227
}
222228

229+
/* Injects a comment line to exercise lexer comment skipping */
223230
fn add_comment(src: &str, rng: &mut Rng) -> String {
224231
let comment = format!("# {}", rand_int(rng));
225232
let mut lines: Vec<&str> = src.lines().collect();
@@ -278,10 +285,11 @@ impl Perf {
278285

279286
enum Outcome { Crash, ParseErr, VmErr, Timeout, Clean(u128, Duration, Duration, Duration) }
280287

288+
/* Runs lex→parse→VM in an isolated thread; catches panics and enforces VM_TIMEOUT */
281289
fn run_once(src: &str) -> Outcome {
282290
let src = if src.len() > MAX_LEN { src[..MAX_LEN].to_string() } else { src.to_string() };
283291
let (tx, rx) = mpsc::channel();
284-
thread::spawn(move || {
292+
thread::Builder::new().stack_size(8 * 1024 * 1024).spawn(move || {
285293
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(|| {
286294
let t0 = Instant::now();
287295
let (tokens, _) = lex(&src);
@@ -309,6 +317,7 @@ fn run_once(src: &str) -> Outcome {
309317
rx.recv_timeout(VM_TIMEOUT).unwrap_or(Outcome::Timeout)
310318
}
311319

320+
/* Coverage-guided seed pool; retains inputs that reach new opcodes */
312321
struct Corpus { entries: Vec<String>, seen: u128 }
313322

314323
impl Corpus {
@@ -324,6 +333,7 @@ impl Corpus {
324333
}
325334
}
326335

336+
/* Run counters and start time for the periodic progress display */
327337
struct Stats { iters: u64, crashes: u64, adds: u64, timeouts: u64, start: Instant }
328338

329339
impl Stats {

compiler/src/modules/parser/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub struct Parser<'src, I: Iterator<Item = Token>> {
6161
// `true=for` (PopIter on break), false=while; parallels loop_starts/loop_breaks.
6262
pub(super) loop_kinds: Vec<bool>,
6363
pub(super) expr_depth: usize,
64+
pub(super) block_depth: usize,
6465
pub(super) saw_newline: bool,
6566
/* True inside f-string brace expr; disables `=` assignment so `f"{x=}"` parses as debug form. */
6667
pub(super) in_fstring_expr: bool,
@@ -459,6 +460,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
459460
saw_newline: false,
460461
in_fstring_expr: false,
461462
expr_depth: 0,
463+
block_depth: 0,
462464
last_line: 0,
463465
last_end: 0,
464466
bracket_stack: Vec::new(),

compiler/src/modules/parser/stmt.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::s;
22

33
use super::Parser;
4-
use super::types::OpCode;
4+
use super::types::{OpCode, MAX_BLOCK_DEPTH};
55

66
use crate::modules::lexer::{Token, TokenType};
77

@@ -331,6 +331,15 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
331331

332332
/* Compiles Indent/Dedent block; is_body=true stops after ReturnValue to skip dead code. */
333333
fn compile_block_inner(&mut self, is_body: bool) {
334+
if self.block_depth >= MAX_BLOCK_DEPTH {
335+
self.errors.push(crate::modules::parser::types::Diagnostic {
336+
msg: crate::s!("nesting too deep"),
337+
start: self.tokens.peek().map_or(0, |t| t.start),
338+
end: self.tokens.peek().map_or(0, |t| t.end),
339+
});
340+
return;
341+
}
342+
self.block_depth += 1;
334343
let indented = self.eat_if(TokenType::Indent);
335344
loop {
336345
while self.eat_if(TokenType::Semi) {}
@@ -349,6 +358,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
349358
if just_returned || !matches!(self.peek(), Some(TokenType::Semi)) { break; }
350359
} else if !matches!(self.peek(), Some(TokenType::Semi)) { break; }
351360
}
361+
self.block_depth -= 1;
352362
}
353363

354364
/* Name-led statement: assign, augmented-op, attr, index, call, or tuple unpack. */

compiler/src/modules/parser/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::modules::vm::types::ExternFn;
55
use alloc::{string::{String, ToString}, vec, vec::Vec};
66

77
pub(crate) const MAX_EXPR_DEPTH: usize = 200;
8+
pub(crate) const MAX_BLOCK_DEPTH: usize = 80;
89
pub(crate) const MAX_INSTRUCTIONS: usize = 65_535;
910

1011
#[derive(Debug, Clone, Copy, PartialEq)]

compiler/src/modules/vm/handlers/builtin_methods/string.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ pub fn rpartition(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
235235
pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
236236
let s = recv_str(vm, recv)?;
237237
if !pos[0].is_int() { return Err(cold_type("center() width must be an integer")); }
238-
let width = pos[0].as_int() as usize;
238+
let width = pos[0].as_int().clamp(0, 1 << 20) as usize;
239239
let fill = if pos.len() > 1 {
240240
val_to_str(vm, pos[1])?.chars().next().unwrap_or(' ')
241241
} else { ' ' };
@@ -251,7 +251,7 @@ pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
251251
pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
252252
if !pos[0].is_int() { return Err(cold_type("zfill() requires an integer argument")); }
253253
let s = recv_str(vm, recv)?;
254-
let width = pos[0].as_int() as usize;
254+
let width = pos[0].as_int().clamp(0, 1 << 20) as usize;
255255
let nchars = s.chars().count();
256256
let out = if nchars >= width {
257257
s

0 commit comments

Comments
 (0)