blob: 742bf55e56750319bebbc96a804f44f10436eb7f [file]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef FUZZTEST_FUZZTEST_INTERNAL_TABLE_OF_RECENT_COMPARES_H_
#define FUZZTEST_FUZZTEST_INTERNAL_TABLE_OF_RECENT_COMPARES_H_
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <limits>
#include <optional>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/random/bit_gen_ref.h"
#include "absl/random/distributions.h"
#include "./fuzztest/internal/type_support.h"
namespace fuzztest::internal {
template <typename ContainerT>
struct DictionaryEntry {
// Index to insert this entry.
std::optional<size_t> position_hint;
// Entry content.
ContainerT value;
};
template <typename T>
T SwapByteOrder(T x) {
static_assert(
sizeof(x) == 1 || sizeof(x) == 2 || sizeof(x) == 4 || sizeof(x) == 8,
"unhandled type for SwapByteOrder");
if constexpr (sizeof(x) == 1) {
return x;
} else if constexpr (sizeof(x) == 2) {
return __builtin_bswap16(x);
} else if constexpr (sizeof(x) == 4) {
return __builtin_bswap32(x);
} else if constexpr (sizeof(x) == 8) {
return __builtin_bswap64(x);
}
}
template <typename ContainerT>
bool operator==(const DictionaryEntry<ContainerT>& lhs,
const DictionaryEntry<ContainerT>& rhs) {
return lhs.position_hint == rhs.position_hint && lhs.value == rhs.value;
}
template <typename H, typename ContainerT>
H AbslHashValue(H h, const DictionaryEntry<ContainerT>& m) {
return H::combine(std::move(h), m.position_hint, m.value);
}
inline size_t ChooseOffset(size_t size, absl::BitGenRef prng) {
return absl::Uniform(prng, size_t{0}, size);
}
inline bool RandomBool(absl::BitGenRef prng) {
return absl::Bernoulli(prng, 0.5);
}
// A fixed-sized table to store integer comparison arguments.
// Note that this is lossy as we randomly hash elements into a fixed
// table, without handling collisions.
template <class T>
class TableOfRecentCompares {
public:
TableOfRecentCompares() {}
// Constructs an integer comparison table with the `compact` option: When it
// is true, `GetRandomOffset()`/`GetRandomEntry()` would sample from inserted
// offsets/entries, unless the table is empty.
explicit TableOfRecentCompares(bool compact) : compact_(compact) {}
// One page size.
static_assert(std::is_integral_v<T> && (sizeof(T) == 1 || sizeof(T) == 2 ||
sizeof(T) == 4 || sizeof(T) == 8),
"TableOfRecentCompares only accepts basic types with size = "
"{1, 2, 4, 8}.");
// Use mask to calculate modulo, avoid % overflow.
static constexpr size_t kTableSize = 4096U / sizeof(T);
static constexpr T kValueMask = static_cast<T>(kTableSize - 1);
void Insert(T lhs, T rhs) {
if (!compact_) {
// Use LCG algorithm generated pesudo random index.
// https://en.wikipedia.org/wiki/Linear_congruential_generator
insert_index_ = (insert_index_ * 37 + 89) & kValueMask;
}
table_[insert_index_].lhs = lhs;
table_[insert_index_].rhs = rhs;
if (compact_ && ++insert_index_ == kTableSize) {
compact_ = false;
}
}
// Returns values from the table that were compared to `val`.
// `min` and `max` are the range limit for returned values.
template <typename ValueType>
std::vector<ValueType> GetMatchingIntegerDictionaryEntries(
ValueType val, ValueType min = std::numeric_limits<ValueType>::min(),
ValueType max = std::numeric_limits<ValueType>::max()) const {
absl::flat_hash_set<T> dictionary_set = {};
// Some simple optimization.
if (min == std::numeric_limits<ValueType>::min() &&
max == std::numeric_limits<ValueType>::max()) {
for (size_t i = 0; i < GetSize(); ++i) {
auto dict_entry = GetMatchingIntegerDictionaryEntry(val, i);
if (dict_entry.has_value()) {
dictionary_set.insert(std::move(*dict_entry));
}
}
} else {
for (size_t i = 0; i < GetSize(); ++i) {
auto dict_entry = GetMatchingIntegerDictionaryEntry(val, i, min, max);
if (dict_entry.has_value()) {
dictionary_set.insert(std::move(*dict_entry));
}
}
}
return std::vector<ValueType>(dictionary_set.begin(), dictionary_set.end());
}
struct CompareEntry {
T lhs;
T rhs;
};
// Returns the matched other-side of a comparison pair:
// val = 5, for comparison pair [5, 10], returns 10.
// if no match, returns std::nullopt.
template <typename ValueType>
std::optional<ValueType> GetMatchingIntegerDictionaryEntry(ValueType val,
size_t idx) const {
std::optional<ValueType> result = std::nullopt;
if (static_cast<ValueType>(table_[idx].lhs) == val) {
result = static_cast<ValueType>(table_[idx].rhs);
} else if (static_cast<ValueType>(table_[idx].rhs) == val) {
result = static_cast<ValueType>(table_[idx].lhs);
}
return result;
}
// Returns the matched other-side in [min, max] or std::nullopt.
template <typename ValueType>
std::optional<ValueType> GetMatchingIntegerDictionaryEntry(
ValueType val, size_t idx, ValueType min, ValueType max) const {
std::optional<ValueType> result =
GetMatchingIntegerDictionaryEntry(val, idx);
if (result.has_value()) {
if (*result < min || *result > max) {
result = std::nullopt;
}
}
return result;
}
const std::array<CompareEntry, kTableSize>& GetTable() const {
return table_;
}
size_t GetSize() const { return compact_ ? insert_index_ : kTableSize; }
size_t GetRandomOffset(absl::BitGenRef prng) const {
const auto size = GetSize();
return size == 0 ? 0 : ChooseOffset(size, prng);
}
CompareEntry GetRandomEntry(absl::BitGenRef prng) const {
return table_[GetRandomOffset(prng)];
}
template <typename ValueType>
std::optional<ValueType> GetRandomSide(absl::BitGenRef prng, size_t idx,
ValueType min, ValueType max) const {
std::optional<ValueType> result = std::nullopt;
const CompareEntry& entry = table_[idx];
if (RandomBool(prng)) {
ValueType val = static_cast<ValueType>(entry.lhs);
if (min <= val && val <= max) {
result = val;
}
} else {
ValueType val = static_cast<ValueType>(entry.lhs);
if (min <= val && val <= max) {
result = val;
}
}
return result;
}
private:
// We compute the next index based on the previous one, so
// we need to store it here.
size_t insert_index_ = 0;
bool compact_ = false;
std::array<CompareEntry, kTableSize> table_ = {};
};
// A fixed-sized table to store buffer comparison arguments.
// I.e., arguments of memcmp, strcmp, strncmp, etc. Note that this
// is lossy as we randomly hash elements into a fixed table, without handling
// collisions. The elements may also be corrupted when it's accessed
// concurrently, but the only effect is to make this table lossy and we
// can accept that. Making this atomic will make the sancov part too heavy
// and not worth it.
class TableOfRecentlyComparedBuffers {
// Use mask to calculate modulo, avoid % overflow.
public:
static constexpr size_t kTableSize = 128;
static constexpr size_t kValueMask = 127;
static constexpr size_t kEntrySize = 128;
TableOfRecentlyComparedBuffers() {}
// Constructs a buffer comparison table with the `compact` option: When it is
// true, `GetRandomOffset()`/`GetRandomEntry()` would sample from inserted
// offsets/entries, unless the table is empty.
explicit TableOfRecentlyComparedBuffers(bool compact) : compact_(compact) {}
void Insert(const uint8_t* buf1, const uint8_t* buf2, size_t n) {
if (!compact_) {
// Use LCG algorithm generated pesudo random index.
// LCG algorithm parameter: (37, 89).
insert_index_ = (insert_index_ * 37 + 89) & kValueMask;
}
if (n >= kEntrySize) n = kEntrySize - 1;
ComparedBufferEntry& entry = table_[insert_index_];
entry.buf_size = n;
std::copy(buf1, buf1 + n, entry.buf1.begin());
std::copy(buf2, buf2 + n, entry.buf2.begin());
if (compact_ && ++insert_index_ == kTableSize) {
compact_ = false;
}
}
// Returns entries from the table that were compared to `val`.
template <typename ContainerT>
std::vector<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntries(const ContainerT& val) const {
using T = value_type_t<ContainerT>;
static_assert(
sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4 || sizeof(T) == 8,
"GetMatchingDictionaryEntries only accepts basic"
" types with size = {1, 2, 4, 8}.");
absl::flat_hash_set<DictionaryEntry<ContainerT>> dictionary = {};
for (size_t i = 0; i < GetSize(); ++i) {
const ComparedBufferEntry& table_entry = table_[i];
const auto dict_entries = GetMatchingContainerDictionaryEntries(
val, table_entry.buf1.data(), table_entry.buf2.data(),
table_entry.buf_size);
dictionary.insert(dict_entries.begin(), dict_entries.end());
}
return std::vector<DictionaryEntry<ContainerT>>(dictionary.begin(),
dictionary.end());
}
// Align the buffer to make sure deferencing reinterpret_cast buf1/buf2
// can read the expected value.
struct ComparedBufferEntry {
size_t buf_size = 0;
alignas(8) std::array<uint8_t, kEntrySize> buf1 = {};
alignas(8) std::array<uint8_t, kEntrySize> buf2 = {};
};
template <typename ContainerT>
static std::vector<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntries(const ContainerT& val,
const uint8_t* buf1,
const uint8_t* buf2, size_t buf_size) {
using T = value_type_t<ContainerT>;
size_t val_size = val.size() * sizeof(T);
static constexpr size_t kBufSizeValueMask = sizeof(T) - 1;
// Filter out some impossible to match cases.
if (((buf_size & kBufSizeValueMask) != 0) || val_size < buf_size ||
buf_size == 0) {
return {};
}
auto get_offsets = [&](const uint8_t* s,
size_t size) -> std::vector<size_t> {
std::vector<size_t> offsets;
auto it = val.begin();
while ((it = std::search(it, val.end(), reinterpret_cast<const T*>(s),
reinterpret_cast<const T*>(s + size))) !=
val.end()) {
offsets.push_back(it - val.begin());
++it;
}
return offsets;
};
std::vector<DictionaryEntry<ContainerT>> entries;
for (const auto offset : get_offsets(buf1, buf_size)) {
entries.push_back({offset, MakeContainer<ContainerT>(buf2, buf_size)});
}
for (const auto offset : get_offsets(buf2, buf_size)) {
entries.push_back({offset, MakeContainer<ContainerT>(buf1, buf_size)});
}
return entries;
}
const std::array<ComparedBufferEntry, kTableSize>& GetTable() const {
return table_;
}
size_t GetSize() const { return compact_ ? insert_index_ : kTableSize; }
size_t GetRandomOffset(absl::BitGenRef prng) const {
const auto size = GetSize();
return size == 0 ? 0 : ChooseOffset(size, prng);
}
const ComparedBufferEntry& GetRandomEntry(absl::BitGenRef prng) const {
return table_[GetRandomOffset(prng)];
}
template <typename ContainerT>
static std::optional<DictionaryEntry<ContainerT>> GetRandomSide(
absl::BitGenRef prng, const uint8_t* buf1, const uint8_t* buf2,
size_t buf_size) {
using T = value_type_t<ContainerT>;
static constexpr size_t kBufSizeValueMask = sizeof(T) - 1;
if ((buf_size & kBufSizeValueMask) != 0 || buf_size == 0) {
return std::nullopt;
}
if (RandomBool(prng)) {
return DictionaryEntry<ContainerT>{
std::nullopt, MakeContainer<ContainerT>(buf1, buf_size)};
} else {
return DictionaryEntry<ContainerT>{
std::nullopt, MakeContainer<ContainerT>(buf2, buf_size)};
}
}
private:
template <typename ContainerT, typename T = value_type_t<ContainerT>>
static ContainerT MakeContainer(const uint8_t* buf, size_t buf_size) {
ContainerT result = {};
std::copy(reinterpret_cast<const T*>(buf),
reinterpret_cast<const T*>(buf + buf_size),
std::back_inserter(result));
return result;
}
size_t insert_index_ = 0;
bool compact_ = false;
std::array<ComparedBufferEntry, kTableSize> table_ = {};
};
class TablesOfRecentCompares {
public:
TablesOfRecentCompares() {}
// Constructs comparison tables with the `compact` option propagated - see the
// corresponding class constructors.
explicit TablesOfRecentCompares(bool compact)
: i8_cmp_table(compact),
i16_cmp_table(compact),
i32_cmp_table(compact),
i64_cmp_table(compact),
mem_cmp_table(compact) {}
template <int I>
const auto& Get() const {
static_assert(I == 0 || I == 1 || I == 2 || I == 4 || I == 8,
"TablesOfRecentCompares::Get "
"should use I in {0, 1, 2, 4, 8}.");
if constexpr (I == 0)
return mem_cmp_table;
else if constexpr (I == 1)
return i8_cmp_table;
else if constexpr (I == 2)
return i16_cmp_table;
else if constexpr (I == 4)
return i32_cmp_table;
else if constexpr (I == 8)
return i64_cmp_table;
}
template <int I>
auto& GetMutable() {
static_assert(I == 0 || I == 1 || I == 2 || I == 4 || I == 8,
"TablesOfRecentCompares::GetMutable "
"should use I in {0, 1, 2, 4, 8}.");
if constexpr (I == 0)
return mem_cmp_table;
else if constexpr (I == 1)
return i8_cmp_table;
else if constexpr (I == 2)
return i16_cmp_table;
else if constexpr (I == 4)
return i32_cmp_table;
else if constexpr (I == 8)
return i64_cmp_table;
}
private:
TableOfRecentCompares<uint8_t> i8_cmp_table = {};
TableOfRecentCompares<uint16_t> i16_cmp_table = {};
TableOfRecentCompares<uint32_t> i32_cmp_table = {};
TableOfRecentCompares<uint64_t> i64_cmp_table = {};
TableOfRecentlyComparedBuffers mem_cmp_table = {};
};
template <typename T>
class IntegerDictionary {
static_assert(std::is_integral_v<T> && (sizeof(T) == 1 || sizeof(T) == 2 ||
sizeof(T) == 4 || sizeof(T) == 8),
"IntegerDictionary only accepts basic types with size = "
"{1, 2, 4, 8}.");
public:
void MatchEntriesFromTableOfRecentCompares(
T val, const TablesOfRecentCompares& torc,
T min = std::numeric_limits<T>::min(),
T max = std::numeric_limits<T>::max()) {
dictionary_ = torc.Get<sizeof(T)>().GetMatchingIntegerDictionaryEntries(
val, min, max);
}
void AddEntry(T val) { dictionary_.push_back(val); }
bool IsEmpty() const { return dictionary_.empty(); }
T GetRandomSavedEntry(absl::BitGenRef prng) const {
return dictionary_[ChooseOffset(dictionary_.size(), prng)];
}
static std::optional<T> GetRandomTORCEntry(T val, absl::BitGenRef prng,
const TablesOfRecentCompares& torc,
T min, T max) {
auto& table = torc.Get<sizeof(T)>();
size_t random_offset = table.GetRandomOffset(prng);
std::optional<T> result =
table.GetMatchingIntegerDictionaryEntry(val, random_offset, min, max);
if (!result.has_value()) {
result = table.GetRandomSide(prng, random_offset, min, max);
}
return result;
}
size_t Size() const { return dictionary_.size(); }
private:
std::vector<T> dictionary_ = {};
};
template <typename ContainerT>
class ContainerDictionary {
static_assert(std::is_integral_v<value_type_t<ContainerT>> &&
(sizeof(value_type_t<ContainerT>) == 1 ||
sizeof(value_type_t<ContainerT>) == 2 ||
sizeof(value_type_t<ContainerT>) == 4 ||
sizeof(value_type_t<ContainerT>) == 8),
"ContainerDictionary only accepts container::value_type being "
"basic types with size = "
"{1, 2, 4, 8}.");
public:
void MatchEntriesFromTableOfRecentCompares(
const ContainerT& val, const TablesOfRecentCompares& torc) {
dictionary_ = torc.Get<0>().GetMatchingContainerDictionaryEntries(val);
// we also try to find entries from TableOfRecentCompares.i32/i64.
AddMatchingIntegerDictionaryEntriesFromTORC(val, torc);
}
bool IsEmpty() const { return dictionary_.empty(); }
void AddEntry(DictionaryEntry<ContainerT>&& val) {
dictionary_.push_back(std::move(val));
}
const DictionaryEntry<ContainerT>& GetRandomSavedEntry(
absl::BitGenRef prng) const {
return dictionary_[ChooseOffset(dictionary_.size(), prng)];
}
static std::optional<DictionaryEntry<ContainerT>> GetRandomTORCEntry(
const ContainerT& val, absl::BitGenRef prng,
const TablesOfRecentCompares& torc) {
using T = value_type_t<ContainerT>;
std::optional<DictionaryEntry<ContainerT>> result = std::nullopt;
// Get from mem_cmp_table or i*_cmp_table with 50/50 probability.
if (RandomBool(prng)) {
auto& memcmp_entry = torc.Get<0>().GetRandomEntry(prng);
const auto matches =
TableOfRecentlyComparedBuffers::GetMatchingContainerDictionaryEntries(
val, memcmp_entry.buf1.data(), memcmp_entry.buf2.data(),
memcmp_entry.buf_size);
if (!matches.empty()) {
result = matches[ChooseOffset(matches.size(), prng)];
} else {
result = TableOfRecentlyComparedBuffers::GetRandomSide<ContainerT>(
prng, memcmp_entry.buf1.data(), memcmp_entry.buf2.data(),
memcmp_entry.buf_size);
}
} else {
const bool swap_byte_order = RandomBool(prng);
if constexpr (sizeof(T) <= 4) {
switch (absl::Uniform(prng, 0, 3)) {
case 0: {
auto i32_entry = torc.Get<4>().GetRandomEntry(prng);
const auto matches =
GetMatchingContainerDictionaryEntriesFromInteger(
val, i32_entry.lhs, i32_entry.rhs, swap_byte_order);
if (!matches.empty()) {
result = matches[ChooseOffset(matches.size(), prng)];
}
} break;
case 1: {
// Somewhere in the past we observe that some implicit type
// promotion will lead to no match found. So we try to cast 64bit
// types to 32bit types and find matches. Demotion is explicit so we
// do not consider that here.
auto i64_entry = torc.Get<8>().GetRandomEntry(prng);
const auto matches =
GetMatchingContainerDictionaryEntriesFromIntegerWithCastTo<
uint32_t>(val, i64_entry.lhs, i64_entry.rhs,
swap_byte_order);
if (!matches.empty()) {
result = matches[ChooseOffset(matches.size(), prng)];
}
} break;
case 2: {
auto i64_entry = torc.Get<8>().GetRandomEntry(prng);
const auto matches =
GetMatchingContainerDictionaryEntriesFromInteger(
val, i64_entry.lhs, i64_entry.rhs, swap_byte_order);
if (!matches.empty()) {
result = matches[ChooseOffset(matches.size(), prng)];
}
} break;
default:
break;
}
} else if constexpr (sizeof(T) <= 8) {
auto i64_entry = torc.Get<8>().GetRandomEntry(prng);
const auto matches = GetMatchingContainerDictionaryEntriesFromInteger(
val, i64_entry.lhs, i64_entry.rhs, swap_byte_order);
if (!matches.empty()) {
result = matches[ChooseOffset(matches.size(), prng)];
}
}
}
return result;
}
size_t Size() const { return dictionary_.size(); }
private:
// Assuming the format and the system running this fuzzer has the
// same endian.
template <typename T>
static std::vector<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntriesFromInteger(const ContainerT& val, T lhs,
T rhs,
bool swap_byte_order) {
if (swap_byte_order) {
lhs = SwapByteOrder(lhs);
rhs = SwapByteOrder(rhs);
}
return TableOfRecentlyComparedBuffers::
GetMatchingContainerDictionaryEntries(
val, reinterpret_cast<const uint8_t*>(&lhs),
reinterpret_cast<const uint8_t*>(&rhs), sizeof(T));
}
template <typename TCastTo, typename T>
static std::vector<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntriesFromIntegerWithCastTo(
const ContainerT& val, T lhs, T rhs, bool swap_byte_order) {
// Ignore overflows.
TCastTo lhs_cast_to = static_cast<TCastTo>(lhs);
TCastTo rhs_cast_to = static_cast<TCastTo>(rhs);
if (swap_byte_order) {
lhs_cast_to = SwapByteOrder(lhs_cast_to);
rhs_cast_to = SwapByteOrder(rhs_cast_to);
}
return TableOfRecentlyComparedBuffers::
GetMatchingContainerDictionaryEntries(
val, reinterpret_cast<const uint8_t*>(&lhs_cast_to),
reinterpret_cast<const uint8_t*>(&rhs_cast_to), sizeof(TCastTo));
}
// Cast integer types into byte arrays and use
// `GetMatchingContainerDictionaryEntry` to find matches in `val`.
void AddMatchingIntegerDictionaryEntriesFromTORC(
const ContainerT& val, const TablesOfRecentCompares& torc) {
using T = value_type_t<ContainerT>;
for (bool swap_byte_order : {false, true}) {
if (val.size() >= 4) {
if constexpr (sizeof(T) <= 4) {
for (auto& i : torc.Get<4>().GetTable()) {
const auto dict_entries_32 =
GetMatchingContainerDictionaryEntriesFromInteger(
val, i.lhs, i.rhs, swap_byte_order);
dictionary_.insert(dictionary_.end(), dict_entries_32.begin(),
dict_entries_32.end());
}
for (auto& i : torc.Get<8>().GetTable()) {
const auto dict_entries_32 =
GetMatchingContainerDictionaryEntriesFromIntegerWithCastTo<
uint32_t>(val, i.lhs, i.rhs, swap_byte_order);
dictionary_.insert(dictionary_.end(), dict_entries_32.begin(),
dict_entries_32.end());
}
}
}
if constexpr (sizeof(T) <= 8) {
if (val.size() >= 8) {
for (auto& i : torc.Get<8>().GetTable()) {
const auto dict_entries_64 =
GetMatchingContainerDictionaryEntriesFromInteger(
val, i.lhs, i.rhs, swap_byte_order);
dictionary_.insert(dictionary_.end(), dict_entries_64.begin(),
dict_entries_64.end());
}
}
}
}
}
std::vector<DictionaryEntry<ContainerT>> dictionary_ = {};
};
} // namespace fuzztest::internal
#endif // FUZZTEST_FUZZTEST_INTERNAL_TABLE_OF_RECENT_COMPARES_H_