Unverified Commit 0e9a4251 authored by Hernando Castano's avatar Hernando Castano Committed by GitHub
Browse files

Add Fuzz Testing to Bump Allocator (#871)

parent eb44ace4
Pipeline #151235 canceled with stages
in 17 minutes and 59 seconds
......@@ -18,7 +18,12 @@ include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
cfg-if = "1.0"
wee_alloc = { version = "0.4", default-features = false, optional = true }
[dev-dependencies]
quickcheck = "1"
quickcheck_macros = "1"
[features]
default = ["std"]
std = []
wee-alloc = ["wee_alloc"]
ink-fuzz-tests = ["std"]
......@@ -121,7 +121,7 @@ impl InnerAlloc {
let alloc_end = alloc_start.checked_add(aligned_size)?;
if alloc_end > self.upper_limit {
let required_pages = (aligned_size + PAGE_SIZE - 1) / PAGE_SIZE;
let required_pages = required_pages(aligned_size)?;
let page_start = self.request_pages(required_pages)?;
self.upper_limit = required_pages
......@@ -137,9 +137,36 @@ impl InnerAlloc {
}
}
/// Calculates the number of pages of memory needed for an allocation of `size` bytes.
///
/// This function rounds up to the next page. For example, if we have an allocation of
/// `size = PAGE_SIZE / 2` this function will indicate that one page is required to satisfy
/// the allocation.
#[inline]
fn required_pages(size: usize) -> Option<usize> {
size.checked_add(PAGE_SIZE - 1)
.and_then(|num| num.checked_div(PAGE_SIZE))
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::size_of;
#[test]
fn can_alloc_no_bytes() {
let mut inner = InnerAlloc::new();
let layout = Layout::new::<()>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = size_of::<()>();
assert_eq!(inner.next, expected_alloc_start);
}
#[test]
fn can_alloc_a_byte() {
......@@ -148,10 +175,11 @@ mod tests {
let layout = Layout::new::<u8>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit = PAGE_SIZE;
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = std::mem::size_of::<u8>();
let expected_alloc_start = size_of::<u8>();
assert_eq!(inner.next, expected_alloc_start);
}
......@@ -166,16 +194,18 @@ mod tests {
}
let layout = Layout::new::<FooBarBaz>();
let mut total_size = 0;
let allocations = 3;
for _ in 0..allocations {
assert!(inner.alloc(layout).is_some());
total_size += layout.pad_to_align().size();
}
let expected_limit = PAGE_SIZE;
let expected_limit = PAGE_SIZE * required_pages(total_size).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = allocations * std::mem::size_of::<FooBarBaz>();
let expected_alloc_start = allocations * size_of::<FooBarBaz>();
assert_eq!(inner.next, expected_alloc_start);
}
......@@ -191,10 +221,11 @@ mod tests {
let layout = Layout::new::<Foo>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit = PAGE_SIZE;
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = std::mem::size_of::<Foo>();
let expected_alloc_start = size_of::<Foo>();
assert_eq!(inner.next, expected_alloc_start);
// Now we'll allocate two bytes which will push us over to the next page
......@@ -206,7 +237,7 @@ mod tests {
// Notice that we start the allocation on the second page, instead of making use of the
// remaining byte on the first page
let expected_alloc_start = PAGE_SIZE + std::mem::size_of::<u16>();
let expected_alloc_start = PAGE_SIZE + size_of::<u16>();
assert_eq!(inner.next, expected_alloc_start);
}
......@@ -221,10 +252,11 @@ mod tests {
let layout = Layout::new::<Foo>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit = 2 * PAGE_SIZE;
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = std::mem::size_of::<Foo>();
let expected_alloc_start = size_of::<Foo>();
assert_eq!(inner.next, expected_alloc_start);
// Now we want to make sure that the state of our allocator is correct for any subsequent
......@@ -235,7 +267,260 @@ mod tests {
let expected_limit = 3 * PAGE_SIZE;
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = 2 * PAGE_SIZE + std::mem::size_of::<u8>();
let expected_alloc_start = 2 * PAGE_SIZE + size_of::<u8>();
assert_eq!(inner.next, expected_alloc_start);
}
}
#[cfg(all(test, feature = "ink-fuzz-tests"))]
mod fuzz_tests {
use super::*;
use quickcheck::{
quickcheck,
TestResult,
};
use std::mem::size_of;
const FROM_SIZE_ALIGN_EXPECT: &str =
"The rounded value of `size` cannot be more than `usize::MAX` since we have
checked that it is a PAGE_SIZE less than `usize::MAX`; Alignment is a
non-zero, power of two.";
#[quickcheck]
fn should_allocate_arbitrary_sized_bytes(n: usize) -> TestResult {
// If `n` is going to overflow we don't want to check it here (we'll check the overflow
// case in another test)
if n.checked_add(PAGE_SIZE - 1).is_none() {
return TestResult::discard()
}
let mut inner = InnerAlloc::new();
let layout =
Layout::from_size_align(n, size_of::<usize>()).expect(FROM_SIZE_ALIGN_EXPECT);
let size = layout.pad_to_align().size();
assert_eq!(
inner.alloc(layout),
Some(0),
"The given pointer for the allocation doesn't match."
);
let expected_alloc_start = size;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let expected_limit = PAGE_SIZE * required_pages(size).unwrap();
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
TestResult::passed()
}
#[quickcheck]
fn should_not_allocate_arbitrary_bytes_if_they_overflow(n: usize) -> TestResult {
// In the previous test we ignored the overflow case, now we ignore the valid cases
if n.checked_add(PAGE_SIZE - 1).is_some() {
return TestResult::discard()
}
if let Ok(layout) = Layout::from_size_align(n, size_of::<usize>()) {
let mut inner = InnerAlloc::new();
assert_eq!(
inner.alloc(layout),
None,
"An allocation which overflows the heap was made."
);
TestResult::passed()
} else {
// We only want to test cases which can create a valid `Layout`
TestResult::discard()
}
}
#[quickcheck]
fn should_allocate_regardless_of_alignment_size(
n: usize,
align: usize,
) -> TestResult {
let aligns = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let align = aligns[align % aligns.len()];
// If `n` is going to overflow we don't want to check it here (we'll check the overflow
// case in another test)
if n.checked_add(PAGE_SIZE - 1).is_none() {
return TestResult::discard()
}
let mut inner = InnerAlloc::new();
let layout = Layout::from_size_align(n, align).expect(FROM_SIZE_ALIGN_EXPECT);
let size = layout.pad_to_align().size();
assert_eq!(
inner.alloc(layout),
Some(0),
"The given pointer for the allocation doesn't match."
);
let expected_alloc_start = size;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let expected_limit = PAGE_SIZE * required_pages(size).unwrap();
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
TestResult::passed()
}
/// The idea behind this fuzz test is to check a series of allocation sequences. For
/// example, we maybe have back to back runs as follows:
///
/// 1. `vec![1, 2, 3]`
/// 2. `vec![4, 5, 6, 7]`
/// 3. `vec![8]`
///
/// Each of the vectors represents one sequence of allocations. Within each sequence the
/// individual size of allocations will be randomly selected by `quickcheck`.
#[quickcheck]
fn should_allocate_arbitrary_byte_sequences(sequence: Vec<usize>) -> TestResult {
let mut inner = InnerAlloc::new();
if sequence.is_empty() {
return TestResult::discard()
}
// We want to make sure no single allocation is going to overflow, we'll check this
// case in a different test
if !sequence
.iter()
.all(|n| n.checked_add(PAGE_SIZE - 1).is_some())
{
return TestResult::discard()
}
// We can't just use `required_pages(Iterator::sum())` here because it ends up
// underestimating the pages due to the ceil rounding at each step
let pages_required = sequence
.iter()
.fold(0, |acc, &x| acc + required_pages(x).unwrap());
let max_pages = required_pages(usize::MAX - PAGE_SIZE + 1).unwrap();
// We know this is going to end up overflowing, we'll check this case in a different
// test
if pages_required > max_pages {
return TestResult::discard()
}
let mut expected_alloc_start = 0;
let mut total_bytes_requested = 0;
let mut total_bytes_fragmented = 0;
for alloc in sequence {
let layout = Layout::from_size_align(alloc, size_of::<usize>())
.expect(FROM_SIZE_ALIGN_EXPECT);
let size = layout.pad_to_align().size();
let current_page_limit = PAGE_SIZE * required_pages(inner.next).unwrap();
let is_too_big_for_current_page = inner.next + size > current_page_limit;
if is_too_big_for_current_page && inner.next != 0 {
let fragmented_in_current_page = if current_page_limit % inner.next == 0 {
current_page_limit - inner.next
} else {
current_page_limit % inner.next
};
total_bytes_fragmented += fragmented_in_current_page;
// We expect our next allocation to be aligned to the start of the next page
// boundary
expected_alloc_start = inner.upper_limit;
}
assert_eq!(
inner.alloc(layout),
Some(expected_alloc_start),
"The given pointer for the allocation doesn't match."
);
total_bytes_requested += size;
expected_alloc_start = total_bytes_requested + total_bytes_fragmented;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let pages_required = required_pages(expected_alloc_start).unwrap();
let expected_limit = PAGE_SIZE * pages_required;
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
}
TestResult::passed()
}
// For this test we have sequences of allocations which will eventually overflow the maximum
// amount of pages (in practice this means our heap will be OOM).
//
// We don't care about the allocations that succeed (those are checked in other tests), we just
// care that eventually an allocation doesn't success.
#[quickcheck]
fn should_not_allocate_arbitrary_byte_sequences_which_eventually_overflow(
sequence: Vec<usize>,
) -> TestResult {
let mut inner = InnerAlloc::new();
if sequence.is_empty() {
return TestResult::discard()
}
// We want to make sure no single allocation is going to overflow, we'll check that
// case seperately
if !sequence
.iter()
.all(|n| n.checked_add(PAGE_SIZE - 1).is_some())
{
return TestResult::discard()
}
// We can't just use `required_pages(Iterator::sum())` here because it ends up
// underestimating the pages due to the ceil rounding at each step
let pages_required = sequence
.iter()
.fold(0, |acc, &x| acc + required_pages(x).unwrap());
let max_pages = required_pages(usize::MAX - PAGE_SIZE + 1).unwrap();
// We want to explicitly test for the case where a series of allocations eventually
// runs out of pages of memory
if !(pages_required > max_pages) {
return TestResult::discard()
}
let mut results = vec![];
for alloc in sequence {
let layout = Layout::from_size_align(alloc, size_of::<usize>())
.expect(FROM_SIZE_ALIGN_EXPECT);
results.push(inner.alloc(layout));
}
// Ensure that at least one of the allocations ends up overflowing our calculations.
assert!(
results.iter().any(|r| r.is_none()),
"Expected an allocation to overflow our heap, but this didn't happen."
);
TestResult::passed()
}
}
......@@ -38,3 +38,7 @@ mod bump;
#[cfg(not(feature = "std"))]
mod handlers;
#[cfg(all(test, feature = "std", feature = "ink-fuzz-tests"))]
#[macro_use(quickcheck)]
extern crate quickcheck_macros;
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment