fuzzgen: Always generate reachable blocks (#5034)

* fuzzgen: Always reachable blocks

* fuzzgen: Rename BlockTerminator

* fuzzgen: Rename `finalize_block`

* fuzzgen: Use `cloned` instead of map clone

Thanks @jameysharp!

Co-authored-by: Jamey Sharp <jamey@minilop.net>

* fuzzgen: `rustfmt`

* fuzzgen: Document paramless targets

* fuzzgen:  Add `BlockTerminatorKind`

* fuzzen: Update BrTable/Switch comment

* fuzzgen: Minor cleanup

Co-authored-by: Jamey Sharp <jamey@minilop.net>
This commit is contained in:
Afonso Bordado
2022-10-17 20:51:20 +01:00
committed by GitHub
parent 1aaea279e5
commit 766ecb561e
3 changed files with 234 additions and 244 deletions

View File

@@ -7,7 +7,7 @@ use cranelift::codegen::ir::instructions::InstructionFormat;
use cranelift::codegen::ir::stackslot::StackSize;
use cranelift::codegen::ir::{types::*, FuncRef, LibCall, UserExternalName, UserFuncName};
use cranelift::codegen::ir::{
AbiParam, Block, ExternalName, Function, JumpTable, Opcode, Signature, StackSlot, Type, Value,
AbiParam, Block, ExternalName, Function, Opcode, Signature, StackSlot, Type, Value,
};
use cranelift::codegen::isa::CallConv;
use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext, Switch, Variable};
@@ -18,6 +18,15 @@ use cranelift::prelude::{
use std::collections::HashMap;
use std::ops::RangeInclusive;
/// Generates a Vec with `len` elements comprised of `options`
fn arbitrary_vec<T: Clone>(
u: &mut Unstructured,
len: usize,
options: &[T],
) -> arbitrary::Result<Vec<T>> {
(0..len).map(|_| u.choose(options).cloned()).collect()
}
type BlockSignature = Vec<Type>;
fn insert_opcode(
@@ -782,157 +791,6 @@ const OPCODE_SIGNATURES: &'static [(
(Opcode::Call, &[], &[], insert_call),
];
type BlockTerminator = fn(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()>;
fn insert_return(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
_source_block: Block,
) -> Result<()> {
let types: Vec<Type> = {
let rets = &builder.func.signature.returns;
rets.iter().map(|p| p.value_type).collect()
};
let vals = fgen.generate_values_for_signature(builder, types.into_iter())?;
builder.ins().return_(&vals[..]);
Ok(())
}
fn insert_jump(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let (block, args) = fgen.generate_target_block(builder, source_block)?;
builder.ins().jump(block, &args[..]);
Ok(())
}
/// Generates a br_table into a random block
fn insert_br_table(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let var = fgen.get_variable_of_type(I32)?; // br_table only supports I32
let val = builder.use_var(var);
let target_blocks = fgen.resources.forward_blocks_without_params(source_block);
let default_block = *fgen.u.choose(target_blocks)?;
// We can still select a backwards branching jump table here!
let tables = fgen.resources.forward_jump_tables(builder, source_block);
let jt = *fgen.u.choose(&tables[..])?;
builder.ins().br_table(val, default_block, jt);
Ok(())
}
/// Generates a brz/brnz into a random block
fn insert_br(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let (block, args) = fgen.generate_target_block(builder, source_block)?;
let condbr_types = [I8, I16, I32, I64, I128, B1];
let _type = *fgen.u.choose(&condbr_types[..])?;
let var = fgen.get_variable_of_type(_type)?;
let val = builder.use_var(var);
if bool::arbitrary(fgen.u)? {
builder.ins().brz(val, block, &args[..]);
} else {
builder.ins().brnz(val, block, &args[..]);
}
// After brz/brnz we must generate a jump
insert_jump(fgen, builder, source_block)?;
Ok(())
}
fn insert_bricmp(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let (block, args) = fgen.generate_target_block(builder, source_block)?;
let cc = *fgen.u.choose(IntCC::all())?;
let _type = *fgen.u.choose(&[I8, I16, I32, I64, I128])?;
let lhs_var = fgen.get_variable_of_type(_type)?;
let lhs_val = builder.use_var(lhs_var);
let rhs_var = fgen.get_variable_of_type(_type)?;
let rhs_val = builder.use_var(rhs_var);
builder
.ins()
.br_icmp(cc, lhs_val, rhs_val, block, &args[..]);
// After bricmp's we must generate a jump
insert_jump(fgen, builder, source_block)?;
Ok(())
}
fn insert_switch(
fgen: &mut FunctionGenerator,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let _type = *fgen.u.choose(&[I8, I16, I32, I64, I128][..])?;
let switch_var = fgen.get_variable_of_type(_type)?;
let switch_val = builder.use_var(switch_var);
// TODO: We should also generate backwards branches in switches
let default_block = {
let target_blocks = fgen.resources.forward_blocks_without_params(source_block);
*fgen.u.choose(target_blocks)?
};
// Build this into a HashMap since we cannot have duplicate entries.
let mut entries = HashMap::new();
for _ in 0..fgen.param(&fgen.config.switch_cases)? {
// The Switch API only allows for entries that are addressable by the index type
// so we need to limit the range of values that we generate.
let (ty_min, ty_max) = _type.bounds(false);
let range_start = fgen.u.int_in_range(ty_min..=ty_max)?;
// We can either insert a contiguous range of blocks or a individual block
// This is done because the Switch API specializes contiguous ranges.
let range_size = if bool::arbitrary(fgen.u)? {
1
} else {
fgen.param(&fgen.config.switch_max_range_size)?
} as u128;
// Build the switch entries
for i in 0..range_size {
let index = range_start.wrapping_add(i) % ty_max;
let block = {
let target_blocks = fgen.resources.forward_blocks_without_params(source_block);
*fgen.u.choose(target_blocks)?
};
entries.insert(index, block);
}
}
let mut switch = Switch::new();
for (entry, block) in entries.into_iter() {
switch.set_entry(entry, block);
}
switch.emit(builder, switch_val, default_block);
Ok(())
}
/// These libcalls need a interpreter implementation in `cranelift-fuzzgen.rs`
const ALLOWED_LIBCALLS: &'static [LibCall] = &[
LibCall::CeilF32,
@@ -952,33 +810,37 @@ where
resources: Resources,
}
#[derive(Debug, Clone)]
enum BlockTerminator {
Return,
Jump(Block),
Br(Block, Block),
BrIcmp(Block, Block),
BrTable(Block, Vec<Block>),
Switch(Type, Block, HashMap<u128, Block>),
}
#[derive(Debug, Clone)]
enum BlockTerminatorKind {
Return,
Jump,
Br,
BrIcmp,
BrTable,
Switch,
}
#[derive(Default)]
struct Resources {
vars: HashMap<Type, Vec<Variable>>,
blocks: Vec<(Block, BlockSignature)>,
blocks_without_params: Vec<Block>,
jump_tables: Vec<JumpTable>,
block_terminators: Vec<BlockTerminator>,
func_refs: Vec<(Signature, FuncRef)>,
stack_slots: Vec<(StackSlot, StackSize)>,
}
impl Resources {
/// Returns [JumpTable]'s where all blocks are forward of `block`
fn forward_jump_tables(&self, builder: &FunctionBuilder, block: Block) -> Vec<JumpTable> {
// TODO: We can avoid allocating a Vec here by sorting self.jump_tables based
// on the minimum block and returning a slice based on that.
// See https://github.com/bytecodealliance/wasmtime/pull/4894#discussion_r971241430 for more details
// Unlike with the blocks below jump table targets are not ordered, thus we do need
// to allocate a Vec here.
let jump_tables = &builder.func.jump_tables;
self.jump_tables
.iter()
.copied()
.filter(|jt| jump_tables[*jt].iter().all(|target| *target > block))
.collect()
}
/// Partitions blocks at `block`. Only blocks that can be targeted by branches are considered.
///
/// The first slice includes all blocks up to and including `block`.
@@ -993,6 +855,12 @@ impl Resources {
target_blocks.split_at(block.as_u32() as usize)
}
/// Returns blocks forward of `block`. Only blocks that can be targeted by branches are considered.
fn forward_blocks(&self, block: Block) -> &[(Block, BlockSignature)] {
let (_, forward_blocks) = self.partition_target_blocks(block);
forward_blocks
}
/// Generates a slice of `blocks_without_params` ahead of `block`
fn forward_blocks_without_params(&self, block: Block) -> &[Block] {
let partition_point = self.blocks_without_params.partition_point(|b| *b <= block);
@@ -1160,13 +1028,7 @@ where
/// Chooses a random block which can be targeted by a jump / branch.
/// This means any block that is not the first block.
///
/// For convenience we also generate values that match the block's signature
fn generate_target_block(
&mut self,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<(Block, Vec<Value>)> {
fn generate_target_block(&mut self, source_block: Block) -> Result<Block> {
// We try to mostly generate forward branches to avoid generating an excessive amount of
// infinite loops. But they are still important, so give them a small chance of existing.
let (backwards_blocks, forward_blocks) =
@@ -1179,9 +1041,17 @@ where
};
assert!(!block_targets.is_empty());
let (block, signature) = self.u.choose(block_targets)?.clone();
let args = self.generate_values_for_signature(builder, signature.into_iter())?;
Ok((block, args))
let (block, _) = self.u.choose(block_targets)?.clone();
Ok(block)
}
fn generate_values_for_block(
&mut self,
builder: &mut FunctionBuilder,
block: Block,
) -> Result<Vec<Value>> {
let (_, sig) = self.resources.blocks[block.as_u32() as usize].clone();
self.generate_values_for_signature(builder, sig.iter().copied())
}
fn generate_values_for_signature<I: Iterator<Item = Type>>(
@@ -1198,48 +1068,77 @@ where
.collect()
}
/// We always need to exit safely out of a block.
/// This either means a jump into another block or a return.
fn finalize_block(&mut self, builder: &mut FunctionBuilder, source_block: Block) -> Result<()> {
let has_jump_tables = !self
.resources
.forward_jump_tables(builder, source_block)
.is_empty();
/// The terminator that we need to insert has already been picked ahead of time
/// we just need to build the instructions for it
fn insert_terminator(
&mut self,
builder: &mut FunctionBuilder,
source_block: Block,
) -> Result<()> {
let terminator = self.resources.block_terminators[source_block.as_u32() as usize].clone();
let has_forward_blocks = {
let (_, forward_blocks) = self.resources.partition_target_blocks(source_block);
!forward_blocks.is_empty()
};
match terminator {
BlockTerminator::Return => {
let types: Vec<Type> = {
let rets = &builder.func.signature.returns;
rets.iter().map(|p| p.value_type).collect()
};
let vals = self.generate_values_for_signature(builder, types.into_iter())?;
let has_forward_blocks_without_params = !self
.resources
.forward_blocks_without_params(source_block)
.is_empty();
builder.ins().return_(&vals[..]);
}
BlockTerminator::Jump(target) => {
let args = self.generate_values_for_block(builder, target)?;
builder.ins().jump(target, &args[..]);
}
BlockTerminator::Br(left, right) => {
let left_args = self.generate_values_for_block(builder, left)?;
let right_args = self.generate_values_for_block(builder, right)?;
let terminators: &[(BlockTerminator, bool)] = &[
// Return is always a valid option
(insert_return, true),
// If we have forward blocks, we can allow generating jumps and branches
(insert_jump, has_forward_blocks),
(insert_br, has_forward_blocks),
(insert_bricmp, has_forward_blocks),
// Switches can only use blocks without params
(insert_switch, has_forward_blocks_without_params),
// We need both jump tables and a default block for br_table
(
insert_br_table,
has_jump_tables && has_forward_blocks_without_params,
),
];
let condbr_types = [I8, I16, I32, I64, I128, B1];
let _type = *self.u.choose(&condbr_types[..])?;
let val = builder.use_var(self.get_variable_of_type(_type)?);
let terminators: Vec<_> = terminators
.into_iter()
.filter(|(_, valid)| *valid)
.map(|(term, _)| term)
.collect();
if bool::arbitrary(self.u)? {
builder.ins().brz(val, left, &left_args[..]);
} else {
builder.ins().brnz(val, left, &left_args[..]);
}
builder.ins().jump(right, &right_args[..]);
}
BlockTerminator::BrIcmp(left, right) => {
let cc = *self.u.choose(IntCC::all())?;
let _type = *self.u.choose(&[I8, I16, I32, I64, I128])?;
let inserter = self.u.choose(&terminators[..])?;
inserter(self, builder, source_block)?;
let lhs = builder.use_var(self.get_variable_of_type(_type)?);
let rhs = builder.use_var(self.get_variable_of_type(_type)?);
let left_args = self.generate_values_for_block(builder, left)?;
let right_args = self.generate_values_for_block(builder, right)?;
builder.ins().br_icmp(cc, lhs, rhs, left, &left_args[..]);
builder.ins().jump(right, &right_args[..]);
}
BlockTerminator::BrTable(default, targets) => {
// Create jump tables on demand
let jt = builder.create_jump_table(JumpTableData::with_blocks(targets));
// br_table only supports I32
let val = builder.use_var(self.get_variable_of_type(I32)?);
builder.ins().br_table(val, default, jt);
}
BlockTerminator::Switch(_type, default, entries) => {
let mut switch = Switch::new();
for (&entry, &block) in entries.iter() {
switch.set_entry(entry, block);
}
let switch_val = builder.use_var(self.get_variable_of_type(_type)?);
switch.emit(builder, switch_val, default);
}
}
Ok(())
}
@@ -1254,27 +1153,6 @@ where
Ok(())
}
fn generate_jumptables(&mut self, builder: &mut FunctionBuilder) -> Result<()> {
// We shouldn't try to generate jumptables if we don't have any valid targets!
if self.resources.blocks_without_params.is_empty() {
return Ok(());
}
for _ in 0..self.param(&self.config.jump_tables_per_function)? {
let mut jt_data = JumpTableData::new();
for _ in 0..self.param(&self.config.jump_table_entries)? {
let block = *self.u.choose(&self.resources.blocks_without_params)?;
jt_data.push_entry(block);
}
self.resources
.jump_tables
.push(builder.create_jump_table(jt_data));
}
Ok(())
}
fn generate_funcrefs(&mut self, builder: &mut FunctionBuilder) -> Result<()> {
let count = self.param(&self.config.funcrefs_per_function)?;
for func_index in 0..count.try_into().unwrap() {
@@ -1396,6 +1274,118 @@ where
.map(|(b, _)| *b)
.collect();
// Compute the block CFG
//
// cranelift-frontend requires us to never generate unreachable blocks
// To ensure this property we start by constructing a main "spine" of blocks. So block1 can
// always jump to block2, and block2 can always jump to block3, etc...
//
// That is not a very interesting CFG, so we introduce variations on that, but always
// ensuring that the property of pointing to the next block is maintained whatever the
// branching mechanism we use.
let blocks = self.resources.blocks.clone();
self.resources.block_terminators = blocks
.iter()
.map(|&(block, _)| {
let next_block = Block::with_number(block.as_u32() + 1).unwrap();
let forward_blocks = self.resources.forward_blocks(block);
let paramless_targets = self.resources.forward_blocks_without_params(block);
let has_paramless_targets = !paramless_targets.is_empty();
let next_block_is_paramless = paramless_targets.contains(&next_block);
let mut valid_terminators = vec![];
if forward_blocks.is_empty() {
// Return is only valid on the last block.
valid_terminators.push(BlockTerminatorKind::Return);
} else {
// If we have more than one block we can allow terminators that target blocks.
// TODO: We could add some kind of BrReturn/BrIcmpReturn here, to explore edges where we exit
// in the middle of the function
valid_terminators.extend_from_slice(&[
BlockTerminatorKind::Jump,
BlockTerminatorKind::Br,
BlockTerminatorKind::BrIcmp,
]);
}
// BrTable and the Switch interface only allow targeting blocks without params
// we also need to ensure that the next block has no params, since that one is
// guaranteed to be picked in either case.
if has_paramless_targets && next_block_is_paramless {
valid_terminators.extend_from_slice(&[
BlockTerminatorKind::BrTable,
BlockTerminatorKind::Switch,
]);
}
let terminator = self.u.choose(&valid_terminators[..])?;
// Choose block targets for the terminators that we picked above
Ok(match terminator {
BlockTerminatorKind::Return => BlockTerminator::Return,
BlockTerminatorKind::Jump => BlockTerminator::Jump(next_block),
BlockTerminatorKind::Br => {
BlockTerminator::Br(next_block, self.generate_target_block(block)?)
}
BlockTerminatorKind::BrIcmp => {
BlockTerminator::BrIcmp(next_block, self.generate_target_block(block)?)
}
// TODO: Allow generating backwards branches here
BlockTerminatorKind::BrTable => {
// Make the default the next block, and then we don't have to worry
// that we can reach it via the targets
let default = next_block;
let target_count = self.param(&self.config.jump_table_entries)?;
let targets = arbitrary_vec(
self.u,
target_count,
self.resources.forward_blocks_without_params(block),
)?;
BlockTerminator::BrTable(default, targets)
}
BlockTerminatorKind::Switch => {
// Make the default the next block, and then we don't have to worry
// that we can reach it via the entries below
let default_block = next_block;
let _type = *self.u.choose(&[I8, I16, I32, I64, I128][..])?;
// Build this into a HashMap since we cannot have duplicate entries.
let mut entries = HashMap::new();
for _ in 0..self.param(&self.config.switch_cases)? {
// The Switch API only allows for entries that are addressable by the index type
// so we need to limit the range of values that we generate.
let (ty_min, ty_max) = _type.bounds(false);
let range_start = self.u.int_in_range(ty_min..=ty_max)?;
// We can either insert a contiguous range of blocks or a individual block
// This is done because the Switch API specializes contiguous ranges.
let range_size = if bool::arbitrary(self.u)? {
1
} else {
self.param(&self.config.switch_max_range_size)?
} as u128;
// Build the switch entries
for i in 0..range_size {
let index = range_start.wrapping_add(i) % ty_max;
let block = *self
.u
.choose(self.resources.forward_blocks_without_params(block))?;
entries.insert(index, block);
}
}
BlockTerminator::Switch(_type, default_block, entries)
}
})
})
.collect::<Result<_>>()?;
Ok(())
}
@@ -1463,7 +1453,6 @@ where
self.generate_blocks(&mut builder, &sig)?;
// Function preamble
self.generate_jumptables(&mut builder)?;
self.generate_funcrefs(&mut builder)?;
self.generate_stack_slots(&mut builder)?;
@@ -1493,7 +1482,8 @@ where
// Generate block instructions
self.generate_instructions(&mut builder)?;
self.finalize_block(&mut builder, block)?;
// Insert a terminator to safely exit the block
self.insert_terminator(&mut builder, block)?;
}
builder.seal_all_blocks();