use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use luaur_rt::{Lua, Result, VmState};
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Rng {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: u32) -> u32 {
(self.next_u64() % n as u64) as u32
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[self.below(xs.len() as u32) as usize]
}
fn chance(&mut self, num: u32, den: u32) -> bool {
self.below(den) < num
}
}
const NAMES: &[&str] = &["a", "b", "c", "d", "e", "f", "g", "h", "x", "y", "z"];
const SAFE_GLOBALS: &[&str] = &["type", "tostring", "tonumber", "select", "rawequal"];
struct Gen {
rng: Rng,
out: String,
budget: i32,
loop_depth: u32,
}
impl Gen {
fn new(seed: u64) -> Gen {
Gen {
rng: Rng::new(seed),
out: String::with_capacity(512),
budget: 220,
loop_depth: 0,
}
}
fn push(&mut self, s: &str) {
self.out.push_str(s);
}
fn name(&mut self) {
let n = *self.rng.pick(NAMES);
self.out.push_str(n);
}
fn out_of_budget(&self) -> bool {
self.budget <= 0
}
fn expr(&mut self) {
self.budget -= 1;
if self.out_of_budget() {
self.atom();
return;
}
match self.rng.below(10) {
0 | 1 => self.atom(),
2 => self.name(),
3 => {
self.expr();
let op = *self.rng.pick(&[
" + ", " - ", " * ", " / ", " % ", " ^ ", " .. ", " == ", " ~= ", " < ",
" <= ", " > ", " >= ", " and ", " or ",
]);
self.push(op);
self.expr();
}
4 => {
let op = *self.rng.pick(&["-", "not ", "#"]);
self.push(op);
self.expr();
}
5 => {
self.push("(");
self.expr();
self.push(")");
}
6 => self.table(),
7 => {
self.name();
if self.rng.chance(1, 2) {
self.push("[");
self.expr();
self.push("]");
} else {
self.push(".");
self.name();
}
}
8 => {
match self.rng.below(3) {
0 => {
let g = *self.rng.pick(SAFE_GLOBALS);
self.push(g);
}
1 => self.name(),
_ => {
self.name();
self.push(":");
self.name();
}
}
self.call_args();
}
_ => self.func_expr(),
}
}
fn atom(&mut self) {
match self.rng.below(6) {
0 => {
let n = self.rng.below(1000);
self.out.push_str(&n.to_string());
}
1 => {
let n = self.rng.below(1000);
let f = self.rng.below(100);
self.out.push_str(&format!("{n}.{f}"));
}
2 => self.push("true"),
3 => self.push("false"),
4 => self.push("nil"),
_ => {
self.push("\"");
let len = self.rng.below(6);
for _ in 0..len {
let c = b"abcdeABCDE01234"[self.rng.below(15) as usize];
self.out.push(c as char);
}
self.push("\"");
}
}
}
fn table(&mut self) {
self.push("{");
let n = self.rng.below(4);
for i in 0..n {
if i > 0 {
self.push(", ");
}
match self.rng.below(3) {
0 => self.expr(),
1 => {
self.name();
self.push(" = ");
self.expr();
}
_ => {
self.push("[");
self.expr();
self.push("] = ");
self.expr();
}
}
}
self.push("}");
}
fn call_args(&mut self) {
self.push("(");
let n = self.rng.below(3);
for i in 0..n {
if i > 0 {
self.push(", ");
}
self.expr();
}
self.push(")");
}
fn func_expr(&mut self) {
self.push("function(");
let n = self.rng.below(3);
for i in 0..n {
if i > 0 {
self.push(", ");
}
self.name();
}
self.push(") ");
let saved = self.loop_depth;
self.loop_depth = 0;
self.block(2);
self.loop_depth = saved;
self.push(" end");
}
fn block(&mut self, max_stmts: u32) {
let n = self.rng.below(max_stmts + 1);
for _ in 0..n {
if self.out_of_budget() {
break;
}
self.stmt();
self.push("\n");
}
if self.rng.chance(1, 4) {
self.push("return");
if self.rng.chance(1, 2) {
self.push(" ");
self.expr();
}
self.push("\n");
}
}
fn stmt(&mut self) {
self.budget -= 1;
if self.out_of_budget() {
self.push("local ");
self.name();
self.push(" = ");
self.atom();
return;
}
match self.rng.below(12) {
0 => {
self.push("local ");
self.name();
self.push(" = ");
self.expr();
}
1 => {
self.name();
let op = *self
.rng
.pick(&[" = ", " += ", " -= ", " *= ", " /= ", " %= ", " ..= "]);
self.push(op);
self.expr();
}
2 => {
self.push("if ");
self.expr();
self.push(" then\n");
self.block(2);
if self.rng.chance(1, 2) {
self.push("else\n");
self.block(2);
}
self.push("end");
}
3 => {
self.push("while ");
self.expr();
self.push(" do\n");
self.loop_depth += 1;
self.block(2);
self.loop_depth -= 1;
self.push("end");
}
4 => {
self.push("for ");
self.name();
self.push(" = ");
self.expr();
self.push(", ");
self.expr();
if self.rng.chance(1, 2) {
self.push(", ");
self.expr();
}
self.push(" do\n");
self.loop_depth += 1;
self.block(2);
self.loop_depth -= 1;
self.push("end");
}
5 => {
self.push("repeat\n");
self.loop_depth += 1;
self.block(2);
self.loop_depth -= 1;
self.push("until ");
self.expr();
}
6 => {
self.push("do\n");
self.block(2);
self.push("end");
}
7 => {
self.push("local function ");
self.name();
self.func_tail();
}
8 => {
self.push("function ");
self.name();
self.func_tail();
}
9 => {
if self.rng.chance(1, 2) {
let g = *self.rng.pick(SAFE_GLOBALS);
self.push(g);
} else {
self.name();
}
self.call_args();
}
10 if self.loop_depth > 0 => self.push("break"),
11 if self.loop_depth > 0 => self.push("continue"),
_ => {
self.name();
self.push(" = ");
self.expr();
}
}
}
fn func_tail(&mut self) {
self.push("(");
let n = self.rng.below(3);
for i in 0..n {
if i > 0 {
self.push(", ");
}
self.name();
}
self.push(")\n");
let saved = self.loop_depth;
self.loop_depth = 0;
self.block(3);
self.loop_depth = saved;
self.push("end");
}
}
fn gen_program(seed: u64) -> String {
let mut g = Gen::new(seed);
g.block(6);
g.out
}
fn corrupt(src: &str, seed: u64) -> String {
let mut rng = Rng::new(seed ^ 0x5151_5151_5151_5151);
let bytes = src.as_bytes();
if bytes.is_empty() {
return String::from("end end");
}
match rng.below(4) {
0 => {
let at = rng.below(bytes.len() as u32) as usize;
String::from_utf8_lossy(&bytes[..at]).into_owned()
}
1 => {
let at = rng.below(bytes.len() as u32) as usize;
let tok = *rng.pick(&["end", ")", "(", "}", "then", "]", "::"]);
let mut s = String::from_utf8_lossy(&bytes[..at]).into_owned();
s.push_str(tok);
s.push_str(&String::from_utf8_lossy(&bytes[at..]));
s
}
2 => {
let at = rng.below(bytes.len() as u32) as usize;
let mut s = String::from_utf8_lossy(&bytes[..at]).into_owned();
s.push_str(&String::from_utf8_lossy(
&bytes[(at + 1).min(bytes.len())..],
));
s
}
_ => {
let at = rng.below(bytes.len() as u32) as usize;
let mut s = src.to_string();
s.push_str(&String::from_utf8_lossy(&bytes[..at]));
s
}
}
}
fn iters(default: u64) -> u64 {
std::env::var("FUZZ_ITERS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn base_seed() -> u64 {
std::env::var("FUZZ_SEED")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0xA5F0_1234_C0DE_0001)
}
fn quiet_panics() {
std::panic::set_hook(Box::new(|_| {}));
}
#[test]
fn fuzz_compile_never_panics() {
quiet_panics();
let n = iters(1500);
for i in 0..n {
let seed = base_seed().wrapping_add(i);
let valid = gen_program(seed);
let src = if i % 4 == 3 {
corrupt(&valid, seed)
} else {
valid
};
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
let lua = Lua::new();
let _ = lua.load(&src).set_name("fuzz").into_function();
}));
assert!(
outcome.is_ok(),
"COMPILE PANICKED on seed {seed}\n---- source ----\n{src}\n----------------"
);
}
}
#[test]
fn fuzz_run_never_panics() {
quiet_panics();
let n = iters(800);
for i in 0..n {
let seed = base_seed() ^ 0x0000_BEEF_0000_0001 ^ i;
let src = gen_program(seed);
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
let lua = Lua::new();
let steps = Arc::new(AtomicU64::new(0));
let counter = steps.clone();
lua.set_interrupt(move |_| -> Result<VmState> {
if counter.fetch_add(1, Ordering::Relaxed) + 1 > 500_000 {
Err(luaur_rt::Error::runtime("fuzz: step limit reached"))
} else {
Ok(VmState::Continue)
}
});
if let Ok(f) = lua.load(&src).set_name("fuzz").into_function() {
let _ = f.call::<()>(()); }
}));
assert!(
outcome.is_ok(),
"RUN PANICKED on seed {seed}\n---- source ----\n{src}\n----------------"
);
}
}