-
Notifications
You must be signed in to change notification settings - Fork 35
Derives
The following is a full list of all proc macro derives available for usage on enums and structs:
- NewFuzzed
- Mutatable
- BinarySerialize
- VariableSizeObject
- FuzzerObject (a generic catchall for all of the above)
- ToPrimitiveU8
- ToPrimitiveU16
- ToPrimitiveU32
- ToPrimitive64
The ToPrimitive*
s generate an implementation of ToPrimitive
with a simple deref of the struct casted as the appropriate data type. Ensure that your enums have #[derive(Copy, Clone)]
for this one.
All fuzzer attributes will be meta items of the lain
attribute. For example: #[lain(ignore)
where ignore
is considered a meta item.
-
bits = N
signals that the given field is a bitfield with the corresponding number of bits. When serializing data, bitfields of the field's type will all be combined into a single field of the appropriate size. e.g. twou8
fields withbits = 4
will be combined into a single 8-bit field during serialization. When generating new structs the field's bounds will also be taken into appropriate consideration. -
ignore
will use the default field initializer (Default::default()
) and will not perform mutation on the field. It will however be serialized. -
min = N
andmax = N specify the field's min/max bounds. When applied to an array this attribute will **forward** the min/max constraints to each of the element's
new_fuzzedor
mutatemethods. This is useful if you have an array of numbers which all need to be within a certain bound. When applied to a
Vec`, the min/max will be used to determine the min/max length of the vec. -
weight_to = "min"|"max"
will skew the RNG towards either the min or max bound. -
initializer = "method()" | 0x00 | <other_valid_rust_tokens_as_string>
will use a static initializer for initializing the field. -
ignore_chance = N
will give the field N% chance of being ignored. If it's ignored theDefault::default()
method is used, otherwise it's mutated the same as any other field.
weight = N
will weight the given enum variant to the given value. When generating a variant, the algorithm is very similar to the example given in rand::distributions::weighted::WeightedIndex
, and will perform similar logic.
Example:
enum Foo {
#[lain(weight = 2)]
A, // 50% chance of picking A
B, // implied weight of 1, 25% chance for B
C, // implied weight of 1, 25% chance for C
}
Currently the only object-specific attribute is #[lain(serialized_size = N)]
. When applied to a struct or enum this will override the standard SerializedSize
derive with a constant value, N
. If an object (or enum variant) is less than this size, the remaining space is filled with null bytes.
Example:
#[lain(serialized_size = 4)]
enum Foo {
A(u8), // 3 null bytes will be added to the end of this data structure when serializing this variant
B(u8),
}
Implements the NewFuzzed
trait for the given struct or enum. For enums this will choose a random variant and initialize it (if it's a non-unit enum type).
For structs, the initialization is somewhat more complicated. To avoid having to implement Default
for all objects, a MaybeUninit<T>
is created. Next, if the object or any of its children contains variable-size objects (this is where the VariableSizeObject derive/trait come in handy), we will initialize each field in a random order. This helps when a max_size
constraint is provided to try getting better distribution of data.
The following is an example of initialization for a single field
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <u64>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref offset, .. } = *x;
offset
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr =
(uninit_struct_ptr as *mut u8).offset(field_offset) as *mut u64;
std::ptr::write(field_ptr, value);
}
In this block we set up the constraints (if applicable) for this individual field, get a newly fuzzed instance of it, and write it to the appropriate offset in the data structure.
After each field is initialized, we perform fixups if appropriate:
if mutator.should_fixup() {
initialized_struct.fixup(mutator);
}
This has a slight chance of ignoring the fixup procedure depending on the mutator mode.
This macro will emit code similar to the following
impl ::lain::traits::NewFuzzed for PacketData {
type RangeType = u8;
fn new_fuzzed<R: ::lain::rand::Rng>(
mutator: &mut ::lain::mutator::Mutator<R>,
mut constraints: Option<&::lain::types::Constraints<Self::RangeType>>,
) -> PacketData {
use ::lain::rand::seq::index::sample;
use std::any::Any;
let mut max_size = if let Some(ref mut constraints) = constraints {
constraints.max_size.clone()
} else {
None
};
let mut uninit_struct = std::mem::MaybeUninit::<PacketData>::uninit();
let uninit_struct_ptr = uninit_struct.as_mut_ptr();
let range = if Self::is_variable_size() {
for i in sample(&mut mutator.rng, 4usize, 4usize).iter() {
match i {
0usize => {
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <UnsafeEnum<PacketType, u32>>::new_fuzzed(
mutator,
constraints.as_ref(),
);
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref typ, .. } = *x;
typ
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr = (uninit_struct_ptr as *mut u8).offset(field_offset)
as *mut UnsafeEnum<PacketType, u32>;
std::ptr::write(field_ptr, value);
}
}
1usize => {
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <u64>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref offset, .. } = *x;
offset
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr =
(uninit_struct_ptr as *mut u8).offset(field_offset) as *mut u64;
std::ptr::write(field_ptr, value);
}
}
2usize => {
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <u64>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref length, .. } = *x;
length
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr =
(uninit_struct_ptr as *mut u8).offset(field_offset) as *mut u64;
std::ptr::write(field_ptr, value);
}
}
3usize => {
let constraints: Option<
::lain::types::Constraints<
<Vec<u8> as ::lain::traits::NewFuzzed>::RangeType,
>,
> = Some(Constraints {
min: Some(0),
max: Some(10),
weighted: ::lain::types::Weighted::None,
max_size: max_size.clone(),
});
let value = <Vec<u8>>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref data, .. } = *x;
data
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr =
(uninit_struct_ptr as *mut u8).offset(field_offset) as *mut Vec<u8>;
std::ptr::write(field_ptr, value);
}
}
_ => ::std::rt::begin_panic(
"internal error: entered unreachable code",
&("example_fuzzer/src/main.rs", 31u32, 69u32),
),
}
}
} else {
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <UnsafeEnum<PacketType, u32>>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref typ, .. } = *x;
typ
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr = (uninit_struct_ptr as *mut u8).offset(field_offset)
as *mut UnsafeEnum<PacketType, u32>;
std::ptr::write(field_ptr, value);
}
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <u64>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref offset, .. } = *x;
offset
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr = (uninit_struct_ptr as *mut u8).offset(field_offset) as *mut u64;
std::ptr::write(field_ptr, value);
}
let constraints = if let Some(ref max_size) = max_size {
let mut constraints = ::lain::types::Constraints::default();
constraints.max_size = Some(max_size.clone());
Some(constraints)
} else {
None
};
let value = <u64>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref length, .. } = *x;
length
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr = (uninit_struct_ptr as *mut u8).offset(field_offset) as *mut u64;
std::ptr::write(field_ptr, value);
}
let constraints: Option<
::lain::types::Constraints<<Vec<u8> as ::lain::traits::NewFuzzed>::RangeType>,
> = Some(Constraints {
min: Some(0),
max: Some(10),
weighted: ::lain::types::Weighted::None,
max_size: max_size.clone(),
});
let value = <Vec<u8>>::new_fuzzed(mutator, constraints.as_ref());
if let Some(ref mut max_size) = max_size {
*max_size -= value.serialized_size();
}
let field_offset = unsafe {
::field_offset::FieldOffset::<PacketData, _>::new(|x| {
let PacketData { ref data, .. } = *x;
data
})
}
.get_byte_offset() as isize;
unsafe {
let field_ptr = (uninit_struct_ptr as *mut u8).offset(field_offset) as *mut Vec<u8>;
std::ptr::write(field_ptr, value);
}
};
let mut initialized_struct = unsafe { uninit_struct.assume_init() };
if mutator.should_fixup() {
initialized_struct.fixup(mutator);
}
initialized_struct
}
}
This generates an implementation of Mutatable
for the given struct/enum and performs in-place mutations of the the object's fields.
The following is an example of code generated from this macro:
impl ::lain::traits::Mutatable for PacketData {
#[allow(unused)]
fn mutate<R: ::lain::rand::Rng>(
&mut self,
mutator: &mut ::lain::mutator::Mutator<R>,
constraints: Option<&Constraints<u8>>,
) {
<UnsafeEnum<PacketType, u32>>::mutate(&mut self.typ, mutator, constraints);
if mutator.should_early_bail_mutation() {
if mutator.should_fixup() {
<UnsafeEnum<PacketType, u32>>::fixup(&mut self.typ, mutator);
}
return;
}
<u64>::mutate(&mut self.offset, mutator, constraints);
if mutator.should_early_bail_mutation() {
if mutator.should_fixup() {
<u64>::fixup(&mut self.offset, mutator);
}
return;
}
<u64>::mutate(&mut self.length, mutator, constraints);
if mutator.should_early_bail_mutation() {
if mutator.should_fixup() {
<u64>::fixup(&mut self.length, mutator);
}
return;
}
<Vec<u8>>::mutate(&mut self.data, mutator, constraints);
if mutator.should_early_bail_mutation() {
if mutator.should_fixup() {
<Vec<u8>>::fixup(&mut self.data, mutator);
}
return;
}
if mutator.should_fixup() {
self.fixup(mutator);
}
}
}
Note the consistent checks for should_early_bail_mutation
. In some mutator modes it may be useful to only perform fuzzing of a single field. This allows us to perform bailing out early if we've mutated the desired field already.
Generates an implementation of BinarySerialize
that will serialize the given object to a slice, as well as generating an implementation of SerializedSize
. The coupling is strange, but there was never a scenario encountered where you would want one and not the either, so we opted to couple them together.
The following is an example of code generated from this macro:
impl ::lain::traits::BinarySerialize for PacketData {
fn binary_serialize<W: std::io::Write, E: ::lain::byteorder::ByteOrder>(&self, buffer: &mut W) {
use ::lain::byteorder::{BigEndian, LittleEndian, WriteBytesExt};
use ::lain::traits::SerializedSize;
let mut bitfield: u64 = 0;
self.typ.binary_serialize::<_, E>(buffer);
self.offset.binary_serialize::<_, E>(buffer);
self.length.binary_serialize::<_, E>(buffer);
self.data.binary_serialize::<_, E>(buffer);
}
}
For bitfields, we would use the bitfield
variable and push it once the corresponding bitfield has been properly set.
Specializes the implementation of VariableSizeObject
for enums/structs. This is useful for the NewFuzzed implementation.
impl ::lain::traits::VariableSizeObject for PacketData {
fn is_variable_size() -> bool {
false
|| <UnsafeEnum<PacketType, u32>>::is_variable_size()
|| <u64>::is_variable_size()
|| <u64>::is_variable_size()
|| <Vec<u8>>::is_variable_size()
}
}