blob: a8d8a07748157c4fcd8986d95d92edd2d41f82be [file] [log] [blame]
// 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 <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/random/distributions.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 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);
}
template <typename PRNG>
size_t ChooseOffset(size_t size, PRNG& prng) {
return absl::Uniform(prng, size_t{0}, size);
}
template <typename PRNG>
bool RandomBool(PRNG& 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:
// 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 = kTableSize - 1;
// Use LCG algorithm generated pesudo random index.
// https://en.wikipedia.org/wiki/Linear_congruential_generator
void Insert(T lhs, T rhs) {
insert_index_ = (insert_index_ * 37 + 89) & kValueMask;
table_[insert_index_].lhs = lhs;
table_[insert_index_].rhs = rhs;
}
// 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 (int i = 0; i < kTableSize; ++i) {
auto dict_entry = GetMatchingIntegerDictionaryEntry(val, i);
if (dict_entry.has_value()) {
dictionary_set.insert(std::move(*dict_entry));
}
}
} else {
for (int i = 0; i < kTableSize; ++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_;
}
template <typename PRNG>
CompareEntry GetRandomEntry(PRNG& prng) const {
return table_[ChooseOffset(kTableSize, prng)];
}
template <typename PRNG, typename ValueType>
std::optional<ValueType> GetRandomSide(PRNG& 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;
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;
// Use LCG algorithm generated pesudo random index.
void Insert(const uint8_t* buf1, const uint8_t* buf2, size_t n) {
// 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());
}
// Returns entries from the table that were compared to `val`.
template <typename ContainerT>
std::vector<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntries(const ContainerT& val) const {
using T = typename ContainerT::value_type;
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 (const ComparedBufferEntry& table_entry : table_) {
auto dict_entry = GetMatchingContainerDictionaryEntry(
val, table_entry.buf1.data(), table_entry.buf2.data(),
table_entry.buf_size);
if (dict_entry.has_value()) {
dictionary.insert(std::move(*dict_entry));
}
}
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::optional<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntry(const ContainerT& val,
const uint8_t* buf1, const uint8_t* buf2,
size_t buf_size) {
using T = typename ContainerT::value_type;
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 std::nullopt;
}
auto offset1 =
std::search(val.begin(), val.end(), reinterpret_cast<const T*>(buf1),
reinterpret_cast<const T*>(buf1 + buf_size));
if (offset1 != val.end()) {
return DictionaryEntry<ContainerT>{
size_t(offset1 - val.begin()),
MakeContainer<ContainerT>(buf2, buf_size)};
} else {
auto offset2 =
std::search(val.begin(), val.end(), reinterpret_cast<const T*>(buf2),
reinterpret_cast<const T*>(buf2 + buf_size));
if (offset2 != val.end()) {
return DictionaryEntry<ContainerT>{
size_t(offset2 - val.begin()),
MakeContainer<ContainerT>(buf1, buf_size)};
}
}
return std::nullopt;
}
template <typename PRNG>
const ComparedBufferEntry& GetRandomEntry(PRNG& prng) const {
return table_[ChooseOffset(kTableSize, prng)];
}
template <typename ContainerT, typename PRNG>
static std::optional<DictionaryEntry<ContainerT>> GetRandomSide(
PRNG& prng, const uint8_t* buf1, const uint8_t* buf2, size_t buf_size) {
using T = typename ContainerT::value_type;
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 = typename ContainerT::value_type>
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;
std::array<ComparedBufferEntry, kTableSize> table_ = {};
};
class TablesOfRecentCompares {
public:
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(); }
template <typename PRNG>
T GetRandomSavedEntry(PRNG& prng) const {
return dictionary_[ChooseOffset(dictionary_.size(), prng)];
}
template <typename PRNG>
static std::optional<T> GetRandomTORCEntry(T val, PRNG& prng,
const TablesOfRecentCompares& torc,
T min, T max) {
auto& table = torc.Get<sizeof(T)>();
size_t random_offset = ChooseOffset(table.kTableSize, 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<typename ContainerT::value_type> &&
(sizeof(typename ContainerT::value_type) == 1 ||
sizeof(typename ContainerT::value_type) == 2 ||
sizeof(typename ContainerT::value_type) == 4 ||
sizeof(typename ContainerT::value_type) == 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));
}
template <typename PRNG>
const DictionaryEntry<ContainerT>& GetRandomSavedEntry(PRNG& prng) const {
return dictionary_[ChooseOffset(dictionary_.size(), prng)];
}
template <typename PRNG>
static std::optional<DictionaryEntry<ContainerT>> GetRandomTORCEntry(
const ContainerT& val, PRNG& prng, const TablesOfRecentCompares& torc) {
using T = typename ContainerT::value_type;
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);
result =
TableOfRecentlyComparedBuffers::GetMatchingContainerDictionaryEntry(
val, memcmp_entry.buf1.data(), memcmp_entry.buf2.data(),
memcmp_entry.buf_size);
if (!result.has_value()) {
result = TableOfRecentlyComparedBuffers::GetRandomSide<ContainerT>(
prng, memcmp_entry.buf1.data(), memcmp_entry.buf2.data(),
memcmp_entry.buf_size);
}
} else {
if constexpr (sizeof(T) <= 4) {
switch (absl::Uniform(prng, 0, 3)) {
case 0: {
auto i32_entry = torc.Get<4>().GetRandomEntry(prng);
result = GetMatchingContainerDictionaryEntryFromInteger(
val, i32_entry.lhs, i32_entry.rhs);
} 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);
result = GetMatchingContainerDictionaryEntryFromIntegerWithCastTo<
uint32_t>(val, i64_entry.lhs, i64_entry.rhs);
} break;
case 2: {
auto i64_entry = torc.Get<8>().GetRandomEntry(prng);
result = GetMatchingContainerDictionaryEntryFromInteger(
val, i64_entry.lhs, i64_entry.rhs);
} break;
default:
break;
}
} else if constexpr (sizeof(T) <= 8) {
auto i64_entry = torc.Get<8>().GetRandomEntry(prng);
result = GetMatchingContainerDictionaryEntryFromInteger(
val, i64_entry.lhs, i64_entry.rhs);
}
}
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::optional<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntryFromInteger(const ContainerT& val, T lhs,
T rhs) {
return TableOfRecentlyComparedBuffers::GetMatchingContainerDictionaryEntry(
val, reinterpret_cast<const uint8_t*>(&lhs),
reinterpret_cast<const uint8_t*>(&rhs), sizeof(T));
}
template <typename TCastTo, typename T>
static std::optional<DictionaryEntry<ContainerT>>
GetMatchingContainerDictionaryEntryFromIntegerWithCastTo(
const ContainerT& val, T lhs, T rhs) {
// Ignore overflows.
TCastTo lhs_cast_to = static_cast<TCastTo>(lhs);
TCastTo rhs_cast_to = static_cast<TCastTo>(rhs);
return TableOfRecentlyComparedBuffers::GetMatchingContainerDictionaryEntry(
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 = typename ContainerT::value_type;
if constexpr (sizeof(T) <= 4) {
if (val.size() >= 4) {
for (auto& i : torc.Get<4>().GetTable()) {
auto dict_entry_32 =
GetMatchingContainerDictionaryEntryFromInteger(val, i.lhs, i.rhs);
if (dict_entry_32.has_value()) {
dictionary_.push_back(std::move(*dict_entry_32));
}
}
for (auto& i : torc.Get<8>().GetTable()) {
auto dict_entry_32 =
GetMatchingContainerDictionaryEntryFromIntegerWithCastTo<
uint32_t>(val, i.lhs, i.rhs);
if (dict_entry_32.has_value()) {
dictionary_.push_back(std::move(*dict_entry_32));
}
}
}
}
if constexpr (sizeof(T) <= 8) {
if (val.size() >= 8) {
for (auto& i : torc.Get<8>().GetTable()) {
auto dict_entry_64 =
GetMatchingContainerDictionaryEntryFromInteger(val, i.lhs, i.rhs);
if (dict_entry_64.has_value()) {
dictionary_.push_back(std::move(*dict_entry_64));
}
}
}
}
}
std::vector<DictionaryEntry<ContainerT>> dictionary_ = {};
};
} // namespace fuzztest::internal
#endif // FUZZTEST_FUZZTEST_INTERNAL_TABLE_OF_RECENT_COMPARES_H_