makepad/platform/script/compute/tests/compiler_fuzz.rs
Admin 62d4f31b1e script: Splash VM heaps, isolates and lazy system modules
Every heap ref carries its heap tag, generation checks always on, rooted
async values. Splash isolates get a domain table and curated minimal
preludes; system modules under `mod` expand lazily on first touch. No
wall-clock script caps: isolation and force-stop instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

194 lines
9 KiB
Rust

//! Compiler fuzz over hostile source: truncated, spliced and mutated
//! kernels never panic the compiler, and every kernel that does compile is
//! admitted (or refused) without panicking and then runs natively and
//! interpreted with bit-equal results, never writing outside its buffers
//! (guard words on both sides of every buffer).
use makepad_script_compute_sched::kernel::KernelExt;
use makepad_script_compute_sched::admission::{DeviceLimits, JobBudget, Ledger, Origin};
use makepad_script_compute::boundary::{LayoutFieldView, LayoutView};
use makepad_script_compute::kernel::{compile_with, Access, FieldTy, Kernel};
use makepad_script_compute::Backend;
const CORPUS: &[&str] = &[
"let W = 64\nlet pos = output(vec3)\nlet amp = param(8.0)\nfn vertex(i) { let x = float(i % W)\n let z = float(i / W)\n pos[i] = vec3(x, fbm2(vec2(x, z) * 0.02, 3, 2.0, 0.5) * amp, z) }",
"let v = input(f32)\nfn reduce_sum(i) { v[i] * 2.0 }",
"let seg = emit_buffer(2, 4)\nfn primitive(i) { for k in 0..(i % 3) { emit(seg, float(i), float(k)) } }",
"let inst = output(Inst)\nfn instance(i) { inst[i].pos = vec3(float(i), 1.0, 2.0)\n inst[i].phase = 0.5\n inst[i].id = i * 2 }",
"let src = input(f32)\nlet dst = output(f32)\nfn element(i) { dst[i * 7919 - 100000] = src[-i * 1000003] + src[2147483647] }",
"let o = output(vec4, 8, 2)\nfn element(i) { o[i] = vec4(float(i), hash01(i, 1), 0.5, -float(i)) }",
"let s = emit_buffer(Inst, 2)\nfn primitive(i) { let r = Inst{}\n r.pos = vec3(float(i), 1.0, 2.0)\n r.phase = 0.5\n emit(s, r) }",
"let T = [1.0, 2.0, 3.0, 4.0]\nlet o = output(f32)\nfn element(i) { let s = 0.0\n let k = 0\n while k < i { s = s + T[k % 4]\n k = k + 1\n if s > 10.0 { break } }\n o[i] = s }",
"let o = output(mat4)\nlet t = param(0.0, -10, 10)\nfn instance(i) { let q = quat_axis_angle(vec3(0.0, 1.0, 0.0), t + float(i))\n o[i] = mat4_trs(vec3(float(i)), q, vec3(1.0)) }",
"let h = input(f32)\nlet o = output(vec3)\nfn helper(a, b) { if a > b { a - b } elif a < b { b - a } else { 0.0 } }\nfn element(i) { let x = match i % 3 { 0 => 1.0, 1 => 2.0, _ => 3.0 }\n o[i] = vec3(helper(x, h[i]), sample_grid(h, 8, 8, x, 0.5), time) }",
"let o = output(i32)\nfn element(i) { let a = i / 0\n let b = i % 0\n let c = (i << 40) >>> 33\n o[i] = a + b + c + int(1e30) + int(0.0 / 0.0) }",
];
const SPLICES: &[&str] = &[
"for", "while", "loop", "break", "continue", "return", "if", "else", "match", "fn", "let", "{", "}", "(", ")", "[", "]", ",", ".", "=",
"0..", "0..1000000000", "1e38", "-2147483648", "2147483647", "0.0/0.0", "/0", "%0", "<<", ">>>", "i", "i * 4294967", "emit(", "output(",
"input(", "emit_buffer(", "param(", "vec4(", "mat4", "Inst", "Inst{}", ".xyzw", ".q", "float(", "int(", "fbm2(", "hash(", "count", "time",
"seed", "[1.0, 2.0]", "[0.0; 100000000]", "fn f(x) { f(x) }", "\"", "'", "#", "@", "\n", " ", "_",
];
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n.max(1) as u64) as usize
}
}
fn layouts() -> Vec<LayoutView<'static>> {
const FIELDS: &[LayoutFieldView<'static>] = &[
LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 0 },
LayoutFieldView { name: "phase", ty: FieldTy::F32, offset: 4 },
LayoutFieldView { name: "id", ty: FieldTy::I32, offset: 5 },
];
vec![LayoutView { name: "Inst", stride: 8, fields: FIELDS }]
}
fn char_boundary(s: &str, mut at: usize) -> usize {
at = at.min(s.len());
while !s.is_char_boundary(at) {
at -= 1;
}
at
}
const NUMBERS: &[&str] = &["0", "1", "-1", "3", "4096", "100000", "2147483647", "-2147483648", "1e38", "-1e38", "0.5", "1e-45", "65536", "1073741823"];
fn mutate(r: &mut Rng, src: &str) -> String {
let mut s = src.to_string();
let edits = if r.below(10) < 7 { 1 } else { 2 + r.below(2) };
for _ in 0..edits {
let at = char_boundary(&s, r.below(s.len() + 1));
match r.below(8) {
// Truncate.
0 => s.truncate(at),
// Delete a run.
1 => {
let end = char_boundary(&s, at + 1 + r.below(12));
s.replace_range(at..end.max(at), "");
}
// Splice a token.
2 | 3 => s.insert_str(at, SPLICES[r.below(SPLICES.len())]),
// Swap a number literal for a hostile one.
4..=6 => {
let digits: Vec<usize> = s.char_indices().filter(|(_, c)| c.is_ascii_digit()).map(|(k, _)| k).collect();
if digits.is_empty() {
continue;
}
let start = digits[r.below(digits.len())];
let end = s[start..].find(|c: char| !(c.is_ascii_digit() || c == '.')).map_or(s.len(), |e| start + e);
s.replace_range(start..end, NUMBERS[r.below(NUMBERS.len())]);
}
// A random printable byte.
_ => s.insert(at, (b' ' + r.below(95) as u8) as char),
}
}
s
}
const GUARD: u32 = 0xDEAD_BEEF;
const ELEMENTS: usize = 3;
/// Runs `k` on `ELEMENTS` elements with small guarded buffers; returns every
/// buffer's words, or None when the call refuses to run.
fn run(k: &Kernel, interp: bool) -> Option<Vec<Vec<u32>>> {
let mut bufs: Vec<Vec<u32>> = k
.buffers()
.iter()
.map(|b| {
let per = match b.access {
Access::Emit { width, capacity } => (width * capacity) as usize,
_ => b.stride as usize,
};
let words = (per * ELEMENTS).clamp(1, 1 << 16) + 5;
let mut v = vec![GUARD; words + 2];
for (w, x) in v[1..=words].iter_mut().enumerate() {
*x = (w as f32 * 0.25).to_bits();
}
v
})
.collect();
{
let mut call = k.call();
let (_, rest) = bufs.split_at_mut(1);
for (d, v) in k.buffers().iter().skip(1).zip(rest.iter_mut()) {
let n = v.len();
let inner = &mut v[1..n - 1];
let r = match d.access {
Access::Read => call.input_u32(&d.name, &*inner),
_ => call.output_u32(&d.name, inner),
};
r.unwrap_or_else(|e| panic!("binding `{}`: {}", d.name, e));
}
let r = if interp { call.run_interp(ELEMENTS) } else { call.run(ELEMENTS) };
r.ok()?;
}
for (d, v) in k.buffers().iter().zip(&bufs).skip(1) {
assert!(v[0] == GUARD && v[v.len() - 1] == GUARD, "buffer `{}` was written outside its bounds", d.name);
}
Some(bufs.into_iter().skip(1).collect())
}
#[test]
fn hostile_source_never_panics_and_what_compiles_runs_bit_equal() {
let layouts = layouts();
for (n, src) in CORPUS.iter().enumerate() {
compile_with(src, &layouts, Backend::Native).unwrap_or_else(|e| panic!("corpus {} does not compile: {:?}", n, e));
}
// Untrusted jobs whose worst case passes 2e8 ops are refused: the fuzz
// runs only what finishes quickly.
let ledger = Ledger::new(DeviceLimits { untrusted_worst_case: 200_000_000, ..DeviceLimits::default() });
let budget = JobBudget { work: 200_000_000 };
// FUZZ_SEED / FUZZ_ROUNDS explore further than the default run.
let env = |k: &str, d: u64| std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d);
let mut r = Rng(env("FUZZ_SEED", 0x243F_6A88_85A3_08D3) | 1);
let (mut compiled, mut ran, mut refused) = (0, 0, 0);
let rounds = env("FUZZ_ROUNDS", 20000) as usize;
for round in 0..rounds {
let base = CORPUS[r.below(CORPUS.len())];
let src = mutate(&mut r, base);
let result = std::panic::catch_unwind(|| compile_with(&src, &layouts, Backend::Native));
let k = match result {
Err(_) => panic!("round {}: the compiler panicked on:\n{}", round, src),
Ok(Err(_)) => continue,
Ok(Ok(k)) => k,
};
compiled += 1;
// Untrusted admission first: a job it refuses never runs.
if ledger.admit(&k, ELEMENTS, Origin::Ai, &budget).is_err() {
refused += 1;
continue;
}
let native = run(&k, false);
let interp = run(&k, true);
assert_eq!(native, interp, "round {}: native and interpreter differ on:\n{}", round, src);
ran += native.is_some() as usize;
}
eprintln!("{} mutants: {} compiled, {} refused by admission, {} ran bit-equal", rounds, compiled, refused, ran);
assert!(compiled > rounds / 10, "the mutator should leave a fair share compiling");
}
#[test]
fn every_truncation_of_every_corpus_kernel_is_handled() {
let layouts = layouts();
for src in CORPUS {
let mut at = 0;
while at <= src.len() {
let cut = &src[..char_boundary(src, at)];
if std::panic::catch_unwind(|| compile_with(cut, &layouts, Backend::Interp)).is_err() {
panic!("the compiler panicked on the truncation:\n{}", cut);
}
at += 1;
}
}
}