Path: blob/main/crates/fuzzing/src/generators/table_ops.rs
1693 views
//! Generating series of `table.get` and `table.set` operations.1use mutatis::mutators as m;2use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult};3use serde::{Deserialize, Serialize};4use smallvec::SmallVec;5use std::ops::RangeInclusive;6use wasm_encoder::{7CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection,8GlobalSection, ImportSection, Instruction, Module, RefType, TableSection, TableType,9TypeSection, ValType,10};1112const NUM_PARAMS_RANGE: RangeInclusive<u32> = 0..=10;13const NUM_GLOBALS_RANGE: RangeInclusive<u32> = 0..=10;14const TABLE_SIZE_RANGE: RangeInclusive<u32> = 0..=100;15const NUM_REC_GROUPS_RANGE: RangeInclusive<u32> = 0..=10;16const MAX_OPS: usize = 100;1718/// RecGroup ID struct definition.19#[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Ord, Hash, Default, Serialize, Deserialize)]20pub struct RecGroupId(u32);2122/// Struct types definition.23#[derive(Debug, Default, Serialize, Deserialize)]24pub struct Types {25rec_groups: std::collections::BTreeSet<RecGroupId>,26}2728impl Types {29/// Create a fresh `Types` allocator with no recursive groups defined yet.30pub fn new() -> Self {31Self {32rec_groups: Default::default(),33}34}3536/// Insert a rec-group id. Returns true if newly inserted, false if it already existed.37pub fn insert_rec_group(&mut self, id: RecGroupId) -> bool {38self.rec_groups.insert(id)39}4041/// Iterate over all allocated recursive groups.42pub fn groups(&self) -> impl Iterator<Item = &RecGroupId> {43self.rec_groups.iter()44}45}4647/// Limits controlling the structure of a generated Wasm module.48#[derive(Debug, Default, Serialize, Deserialize)]49pub struct TableOpsLimits {50pub(crate) num_params: u32,51pub(crate) num_globals: u32,52pub(crate) table_size: u32,53pub(crate) num_rec_groups: u32,54}5556impl TableOpsLimits {57fn fixup(&mut self) {58// NB: Exhaustively match so that we remember to fixup any other new59// limits we add in the future.60let Self {61num_params,62num_globals,63table_size,64num_rec_groups,65} = self;6667let clamp = |limit: &mut u32, range: RangeInclusive<u32>| {68*limit = (*limit).clamp(*range.start(), *range.end())69};70clamp(table_size, TABLE_SIZE_RANGE);71clamp(num_params, NUM_PARAMS_RANGE);72clamp(num_globals, NUM_GLOBALS_RANGE);73clamp(num_rec_groups, NUM_REC_GROUPS_RANGE);74}75}7677/// A description of a Wasm module that makes a series of `externref` table78/// operations.79#[derive(Debug, Default, Serialize, Deserialize)]80pub struct TableOps {81pub(crate) limits: TableOpsLimits,82pub(crate) ops: Vec<TableOp>,83pub(crate) types: Types,84}8586impl TableOps {87/// Serialize this module into a Wasm binary.88///89/// The module requires several function imports. See this function's90/// implementation for their exact types.91///92/// The single export of the module is a function "run" that takes93/// `self.num_params` parameters of type `externref`.94///95/// The "run" function does not terminate; you should run it with limited96/// fuel. It also is not guaranteed to avoid traps: it may access97/// out-of-bounds of the table.98pub fn to_wasm_binary(&mut self) -> Vec<u8> {99self.fixup();100101let mut module = Module::new();102103// Encode the types for all functions that we are using.104let mut types = TypeSection::new();105106// 0: "gc"107types.ty().function(108vec![],109// Return a bunch of stuff from `gc` so that we exercise GCing when110// there is return pointer space allocated on the stack. This is111// especially important because the x64 backend currently112// dynamically adjusts the stack pointer for each call that uses113// return pointers rather than statically allocating space in the114// stack frame.115vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],116);117118// 1: "run"119let mut params: Vec<ValType> = Vec::with_capacity(self.limits.num_params as usize);120for _i in 0..self.limits.num_params {121params.push(ValType::EXTERNREF);122}123let results = vec![];124types.ty().function(params, results);125126// 2: `take_refs`127types.ty().function(128vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],129vec![],130);131132// 3: `make_refs`133types.ty().function(134vec![],135vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],136);137138// Import the GC function.139let mut imports = ImportSection::new();140imports.import("", "gc", EntityType::Function(0));141imports.import("", "take_refs", EntityType::Function(2));142imports.import("", "make_refs", EntityType::Function(3));143144// Define our table.145let mut tables = TableSection::new();146tables.table(TableType {147element_type: RefType::EXTERNREF,148minimum: u64::from(self.limits.table_size),149maximum: None,150table64: false,151shared: false,152});153154// Define our globals.155let mut globals = GlobalSection::new();156for _ in 0..self.limits.num_globals {157globals.global(158wasm_encoder::GlobalType {159val_type: wasm_encoder::ValType::EXTERNREF,160mutable: true,161shared: false,162},163&ConstExpr::ref_null(wasm_encoder::HeapType::EXTERN),164);165}166167// Define the "run" function export.168let mut functions = FunctionSection::new();169functions.function(1);170171let mut exports = ExportSection::new();172exports.export("run", ExportKind::Func, 3);173174// Give ourselves one scratch local that we can use in various `TableOp`175// implementations.176let mut func = Function::new(vec![(1, ValType::EXTERNREF)]);177178func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));179for op in &self.ops {180op.insert(&mut func, self.limits.num_params);181}182func.instruction(&Instruction::Br(0));183func.instruction(&Instruction::End);184func.instruction(&Instruction::End);185186// Emit one empty (rec ...) per declared group.187for _ in self.types.groups() {188types.ty().rec(Vec::<wasm_encoder::SubType>::new());189}190191let mut code = CodeSection::new();192code.function(&func);193194module195.section(&types)196.section(&imports)197.section(&functions)198.section(&tables)199.section(&globals)200.section(&exports)201.section(&code);202203module.finish()204}205206/// Computes the abstract stack depth after executing all operations207pub fn abstract_stack_depth(&self, index: usize) -> usize {208debug_assert!(index <= self.ops.len());209let mut stack: usize = 0;210for op in self.ops.iter().take(index) {211let pop = op.operands_len();212let push = op.results_len();213stack = stack.saturating_sub(pop);214stack += push;215}216stack217}218219/// Fixes this test case such that it becomes valid.220///221/// This is necessary because a random mutation (e.g. removing an op in the222/// middle of our sequence) might have made it so that subsequent ops won't223/// have their expected operand types on the Wasm stack224/// anymore. Furthermore, because we serialize and deserialize test cases,225/// and libFuzzer will occasionally mutate those serialized bytes directly,226/// rather than use one of our custom mutations, we have no guarantee that227/// pre-mutation test cases are even valid! Therefore, we always call this228/// method before translating this "AST"-style representation into a raw229/// Wasm binary.230fn fixup(&mut self) {231self.limits.fixup();232233let mut new_ops = Vec::with_capacity(self.ops.len());234let mut stack = 0;235236for mut op in self.ops.iter().copied() {237op.fixup(&self.limits);238239let mut temp = SmallVec::<[_; 4]>::new();240241while stack < op.operands_len() {242temp.push(TableOp::Null());243stack += 1;244}245246temp.push(op);247stack = stack - op.operands_len() + op.results_len();248249new_ops.extend(temp);250}251252// Insert drops to balance the final stack state253for _ in 0..stack {254new_ops.push(TableOp::Drop());255}256257self.ops = new_ops;258}259260/// Attempts to remove the last opcode from the sequence.261///262/// Returns `true` if an opcode was successfully removed, or `false` if the list was already empty.263pub fn pop(&mut self) -> bool {264self.ops.pop().is_some()265}266}267268/// A mutator for the table ops269#[derive(Debug)]270pub struct TableOpsMutator;271272impl Mutate<TableOps> for TableOpsMutator {273fn mutate(&mut self, c: &mut Candidates<'_>, ops: &mut TableOps) -> mutatis::Result<()> {274if !c.shrink() {275c.mutation(|ctx| {276if let Some(idx) = ctx.rng().gen_index(ops.ops.len() + 1) {277let stack = ops.abstract_stack_depth(idx);278let (op, _new_stack_size) = TableOp::generate(ctx, &ops, stack)?;279ops.ops.insert(idx, op);280}281Ok(())282})?;283}284if !ops.ops.is_empty() {285c.mutation(|ctx| {286let idx = ctx287.rng()288.gen_index(ops.ops.len())289.expect("ops is not empty");290ops.ops.remove(idx);291Ok(())292})?;293}294295Ok(())296}297}298299impl DefaultMutate for TableOps {300type DefaultMutate = TableOpsMutator;301}302303impl Default for TableOpsMutator {304fn default() -> Self {305TableOpsMutator306}307}308309impl<'a> arbitrary::Arbitrary<'a> for TableOps {310fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {311let mut session = mutatis::Session::new().seed(u.arbitrary()?);312session313.generate()314.map_err(|_| arbitrary::Error::IncorrectFormat)315}316}317318impl Generate<TableOps> for TableOpsMutator {319fn generate(&mut self, ctx: &mut Context) -> MutResult<TableOps> {320let num_params = m::range(NUM_PARAMS_RANGE).generate(ctx)?;321let num_globals = m::range(NUM_GLOBALS_RANGE).generate(ctx)?;322let table_size = m::range(TABLE_SIZE_RANGE).generate(ctx)?;323324let num_rec_groups = m::range(NUM_REC_GROUPS_RANGE).generate(ctx)?;325326let mut ops = TableOps {327limits: TableOpsLimits {328num_params,329num_globals,330table_size,331num_rec_groups,332},333ops: vec![334TableOp::Null(),335TableOp::Drop(),336TableOp::Gc(),337TableOp::LocalSet(0),338TableOp::LocalGet(0),339TableOp::GlobalSet(0),340TableOp::GlobalGet(0),341],342types: Types::new(),343};344345for i in 0..ops.limits.num_rec_groups {346ops.types.insert_rec_group(RecGroupId(i));347}348349let mut stack: usize = 0;350while ops.ops.len() < MAX_OPS {351let (op, new_stack_len) = TableOp::generate(ctx, &ops, stack)?;352ops.ops.push(op);353stack = new_stack_len;354}355356// Drop any leftover refs on the stack.357for _ in 0..stack {358ops.ops.push(TableOp::Drop());359}360361Ok(ops)362}363}364365macro_rules! define_table_ops {366(367$(368$op:ident $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? : $params:expr => $results:expr ,369)*370) => {371#[derive(Copy, Clone, Debug, Serialize, Deserialize)]372pub(crate) enum TableOp {373$(374$op ( $( $($ty),* )? ),375)*376}377#[cfg(test)]378const OP_NAMES: &'static[&'static str] = &[379$(380stringify!($op),381)*382];383384impl TableOp {385#[cfg(test)]386fn name(&self) -> &'static str {387match self {388$(389Self::$op (..) => stringify!($op),390)*391}392}393394pub fn operands_len(&self) -> usize {395match self {396$(397Self::$op (..) => $params,398)*399}400}401402pub fn results_len(&self) -> usize {403match self {404$(405Self::$op (..) => $results,406)*407}408}409}410411$(412#[allow(non_snake_case, reason = "macro-generated code")]413fn $op(414_ctx: &mut mutatis::Context,415_limits: &TableOpsLimits,416stack: usize,417) -> mutatis::Result<(TableOp, usize)> {418#[allow(unused_comparisons, reason = "macro-generated code")]419{420debug_assert!(stack >= $params);421}422423let op = TableOp::$op(424$($({425let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;426let limit = (limit_fn)(_limits);427debug_assert!(limit > 0);428m::range(0..=limit - 1).generate(_ctx)?429})*)?430);431let new_stack = stack - $params + $results;432Ok((op, new_stack))433}434)*435436impl TableOp {437fn fixup(&mut self, limits: &TableOpsLimits) {438match self {439$(440Self::$op( $( $( $limit_var ),* )? ) => {441$( $(442let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;443let limit = (limit_fn)(limits);444debug_assert!(limit > 0);445*$limit_var = *$limit_var % limit;446)* )?447}448)*449}450}451452fn generate(453ctx: &mut mutatis::Context,454ops: &TableOps,455stack: usize,456) -> mutatis::Result<(TableOp, usize)> {457let mut valid_choices: Vec<458fn(&mut Context, &TableOpsLimits, usize) -> mutatis::Result<(TableOp, usize)>459> = vec![];460$(461#[allow(unused_comparisons, reason = "macro-generated code")]462if stack >= $params $($(463&& {464let limit_fn = $limit as fn(&TableOpsLimits) -> $ty;465let limit = (limit_fn)(&ops.limits);466limit > 0467}468)*)? {469valid_choices.push($op);470}471)*472473let f = *ctx.rng()474.choose(&valid_choices)475.expect("should always have a valid op choice");476477(f)(ctx, &ops.limits, stack)478}479}480};481}482483define_table_ops! {484Gc : 0 => 3,485486MakeRefs : 0 => 3,487TakeRefs : 3 => 0,488489// Add one to make sure that out of bounds table accesses are possible, but still rare.490TableGet(elem_index: |ops| ops.table_size + 1 => u32) : 0 => 1,491TableSet(elem_index: |ops| ops.table_size + 1 => u32) : 1 => 0,492493GlobalGet(global_index: |ops| ops.num_globals => u32) : 0 => 1,494GlobalSet(global_index: |ops| ops.num_globals => u32) : 1 => 0,495496LocalGet(local_index: |ops| ops.num_params => u32) : 0 => 1,497LocalSet(local_index: |ops| ops.num_params => u32) : 1 => 0,498499Drop : 1 => 0,500501Null : 0 => 1,502}503504impl TableOp {505fn insert(self, func: &mut Function, scratch_local: u32) {506let gc_func_idx = 0;507let take_refs_func_idx = 1;508let make_refs_func_idx = 2;509510match self {511Self::Gc() => {512func.instruction(&Instruction::Call(gc_func_idx));513}514Self::MakeRefs() => {515func.instruction(&Instruction::Call(make_refs_func_idx));516}517Self::TakeRefs() => {518func.instruction(&Instruction::Call(take_refs_func_idx));519}520Self::TableGet(x) => {521func.instruction(&Instruction::I32Const(x.cast_signed()));522func.instruction(&Instruction::TableGet(0));523}524Self::TableSet(x) => {525func.instruction(&Instruction::LocalSet(scratch_local));526func.instruction(&Instruction::I32Const(x.cast_signed()));527func.instruction(&Instruction::LocalGet(scratch_local));528func.instruction(&Instruction::TableSet(0));529}530Self::GlobalGet(x) => {531func.instruction(&Instruction::GlobalGet(x));532}533Self::GlobalSet(x) => {534func.instruction(&Instruction::GlobalSet(x));535}536Self::LocalGet(x) => {537func.instruction(&Instruction::LocalGet(x));538}539Self::LocalSet(x) => {540func.instruction(&Instruction::LocalSet(x));541}542Self::Drop() => {543func.instruction(&Instruction::Drop);544}545Self::Null() => {546func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::EXTERN));547}548}549}550}551552#[cfg(test)]553mod tests {554use super::*;555556/// Creates empty TableOps557fn empty_test_ops() -> TableOps {558let mut t = TableOps {559limits: TableOpsLimits {560num_params: 5,561num_globals: 5,562table_size: 5,563num_rec_groups: 5,564},565ops: vec![],566types: Types::new(),567};568for i in 0..t.limits.num_rec_groups {569t.types.insert_rec_group(RecGroupId(i));570}571t572}573574/// Creates TableOps with all default opcodes575fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> TableOps {576let mut t = TableOps {577limits: TableOpsLimits {578num_params,579num_globals,580table_size,581num_rec_groups: 3,582},583ops: vec![584TableOp::Null(),585TableOp::Drop(),586TableOp::Gc(),587TableOp::LocalSet(0),588TableOp::LocalGet(0),589TableOp::GlobalSet(0),590TableOp::GlobalGet(0),591TableOp::Null(),592TableOp::Drop(),593TableOp::Gc(),594TableOp::LocalSet(0),595TableOp::LocalGet(0),596TableOp::GlobalSet(0),597TableOp::GlobalGet(0),598TableOp::Null(),599TableOp::Drop(),600],601types: Types::new(),602};603for i in 0..t.limits.num_rec_groups {604t.types.insert_rec_group(RecGroupId(i));605}606t607}608609#[test]610fn mutate_table_ops_with_default_mutator() -> mutatis::Result<()> {611let _ = env_logger::try_init();612let mut res = test_ops(5, 5, 5);613614let mut session = mutatis::Session::new();615616for _ in 0..1024 {617session.mutate(&mut res)?;618let wasm = res.to_wasm_binary();619620let feats = wasmparser::WasmFeatures::default();621feats.reference_types();622feats.gc();623let mut validator = wasmparser::Validator::new_with_features(feats);624625let wat = wasmprinter::print_bytes(&wasm).expect("[-] Failed .print_bytes(&wasm).");626let result = validator.validate_all(&wasm);627log::debug!("{wat}");628assert!(629result.is_ok(),630"\n[-] Invalid wat: {}\n\t\t==== Failed Wat ====\n{}",631result.err().expect("[-] Failed .err() in assert macro."),632wat633);634}635Ok(())636}637638#[test]639fn every_op_generated() -> mutatis::Result<()> {640let _ = env_logger::try_init();641let mut unseen_ops: std::collections::HashSet<_> = OP_NAMES.iter().copied().collect();642643let mut res = empty_test_ops();644let mut session = mutatis::Session::new();645646'outer: for _ in 0..=1024 {647session.mutate(&mut res)?;648for op in &res.ops {649unseen_ops.remove(op.name());650if unseen_ops.is_empty() {651break 'outer;652}653}654}655656assert!(unseen_ops.is_empty(), "Failed to generate {unseen_ops:?}");657Ok(())658}659660#[test]661fn test_wat_string() -> mutatis::Result<()> {662let _ = env_logger::try_init();663664let mut table_ops = test_ops(2, 2, 5);665666let wasm = table_ops.to_wasm_binary();667668let actual_wat = wasmprinter::print_bytes(&wasm).expect("Failed to convert to WAT");669let actual_wat = actual_wat.trim();670671let expected_wat = r#"672(module673(type (;0;) (func (result externref externref externref)))674(type (;1;) (func (param externref externref)))675(type (;2;) (func (param externref externref externref)))676(type (;3;) (func (result externref externref externref)))677(rec)678(rec)679(rec)680(import "" "gc" (func (;0;) (type 0)))681(import "" "take_refs" (func (;1;) (type 2)))682(import "" "make_refs" (func (;2;) (type 3)))683(table (;0;) 5 externref)684(global (;0;) (mut externref) ref.null extern)685(global (;1;) (mut externref) ref.null extern)686(export "run" (func 3))687(func (;3;) (type 1) (param externref externref)688(local externref)689loop ;; label = @1690ref.null extern691drop692call 0693local.set 0694local.get 0695global.set 0696global.get 0697ref.null extern698drop699call 0700local.set 0701local.get 0702global.set 0703global.get 0704ref.null extern705drop706drop707drop708drop709drop710drop711drop712br 0 (;@1;)713end714)715)716"#;717let expected_wat = expected_wat.trim();718719eprintln!("=== actual ===\n{actual_wat}");720eprintln!("=== expected ===\n{expected_wat}");721assert_eq!(722actual_wat, expected_wat,723"actual WAT does not match expected"724);725726Ok(())727}728729#[test]730fn emits_empty_rec_groups_and_validates() -> mutatis::Result<()> {731let _ = env_logger::try_init();732733let mut ops = TableOps {734limits: TableOpsLimits {735num_params: 2,736num_globals: 1,737table_size: 5,738num_rec_groups: 2,739},740ops: vec![TableOp::Null(), TableOp::Drop()],741types: Types::new(),742};743744for i in 0..ops.limits.num_rec_groups {745ops.types.insert_rec_group(RecGroupId(i));746}747748let wasm = ops.to_wasm_binary();749750let feats = wasmparser::WasmFeatures::default();751feats.reference_types();752feats.gc();753let mut validator = wasmparser::Validator::new_with_features(feats);754assert!(755validator.validate_all(&wasm).is_ok(),756"GC validation failed"757);758759let wat = wasmprinter::print_bytes(&wasm).expect("to WAT");760let recs = wat.matches("(rec").count();761let structs = wat.matches("(struct)").count();762763assert_eq!(recs, 2, "expected 2 (rec) blocks, got {recs}");764// Still keep as zero. Will update in the next PR765assert_eq!(structs, 0, "expected no struct types, got {structs}");766767Ok(())768}769}770771772