Solidity fuzzer: Add simple import statements and test/source state.

This commit is contained in:
Bhargava Shastry 2021-01-27 14:55:30 +01:00
parent 98e7b61a37
commit fa9328cb0e
3 changed files with 175 additions and 82 deletions

View File

@ -61,7 +61,7 @@ size_t SolidityCustomMutatorInterface::generate()
{
string testCase = generator->generateTestProgram();
solAssert(
!testCase.empty() && data,
data,
"Solc custom mutator: Invalid mutant or memory pointer"
);
size_t mutantSize = min(testCase.size(), maxMutantSize - 1);

View File

@ -21,6 +21,12 @@
#include <libsolutil/Whiskers.h>
#include <libsolutil/Visitor.h>
#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm/copy.hpp>
using namespace solidity::test::fuzzer;
using namespace solidity::test::fuzzer::mutator;
using namespace solidity::util;
using namespace std;
@ -36,17 +42,32 @@ string GeneratorBase::visitChildren()
{
ostringstream os;
// Randomise visit order
vector<GeneratorPtr> randomisedChildren;
vector<std::pair<GeneratorPtr, unsigned>> randomisedChildren;
for (auto const& child: generators)
randomisedChildren.push_back(child);
shuffle(randomisedChildren.begin(), randomisedChildren.end(), *uRandDist->randomEngine);
for (auto child: randomisedChildren)
os << std::visit(GenericVisitor{
[&](auto const& _item) { return _item->generate(); }
}, child);
for (auto const& child: randomisedChildren)
if (uRandDist->likely(child.second + 1))
for (unsigned i = 0; i < uRandDist->distributionOneToN(child.second); i++)
os << std::visit(GenericVisitor{
[&](auto const& _item) { return _item->generate(); }
}, child.first);
return os.str();
}
void SourceState::print(std::ostream& _os) const
{
for (auto const& import: importedSources)
_os << "Imports: " << import << std::endl;
}
set<string> TestState::sourceUnitPaths() const
{
set<string> keys;
boost::copy(sourceUnitState | boost::adaptors::map_keys, std::inserter(keys, keys.begin()));
return keys;
}
string TestState::randomPath(set<string> const& _sourceUnitPaths) const
{
auto it = _sourceUnitPaths.begin();
@ -63,14 +84,17 @@ string TestState::randomPath(set<string> const& _sourceUnitPaths) const
string TestState::randomPath() const
{
solAssert(!empty(), "Solc custom mutator: Null test state");
return randomPath(sourceUnitPaths);
return randomPath(sourceUnitPaths());
}
void TestState::print(std::ostream& _os) const
{
_os << "Printing test state" << std::endl;
for (auto const& item: sourceUnitPaths)
_os << "Source path: " << item << std::endl;
for (auto const& item: sourceUnitState)
{
_os << "Source path: " << item.first << std::endl;
item.second->print(_os);
}
}
string TestState::randomNonCurrentPath() const
@ -82,9 +106,10 @@ string TestState::randomNonCurrentPath() const
set<string> filteredSourcePaths;
string currentPath = currentSourceUnitPath;
set<string> sourcePaths = sourceUnitPaths();
copy_if(
sourceUnitPaths.begin(),
sourceUnitPaths.end(),
sourcePaths.begin(),
sourcePaths.end(),
inserter(filteredSourcePaths, filteredSourcePaths.begin()),
[currentPath](string const& _item) {
return _item != currentPath;
@ -96,52 +121,42 @@ string TestState::randomNonCurrentPath() const
void TestCaseGenerator::setup()
{
addGenerators({
mutator->generator<SourceUnitGenerator>()
{mutator->generator<SourceUnitGenerator>(), s_maxSourceUnits}
});
}
string TestCaseGenerator::visit()
{
ostringstream os;
for (unsigned i = 0; i < uRandDist->distributionOneToN(s_maxSourceUnits); i++)
{
string sourcePath = path();
os << "\n"
<< "==== Source: "
<< sourcePath
<< " ===="
<< "\n";
updateSourcePath(sourcePath);
m_numSourceUnits++;
os << visitChildren();
}
return os.str();
return visitChildren();
}
void SourceUnitGenerator::setup()
{
addGenerators({
mutator->generator<ImportGenerator>(),
mutator->generator<PragmaGenerator>()
{mutator->generator<ImportGenerator>(), s_maxImports},
{mutator->generator<PragmaGenerator>(), 1}
});
}
string SourceUnitGenerator::visit()
{
return visitChildren();
state->addSource();
ostringstream os;
os << "\n"
<< "==== Source: "
<< state->currentPath()
<< " ===="
<< "\n";
os << visitChildren();
return os.str();
}
string PragmaGenerator::visit()
{
static constexpr const char* preamble = R"(
pragma solidity >= 0.0.0;
pragma experimental SMTChecker;
)";
// Choose equally at random from coder v1 and v2
string abiPragma = "pragma abicoder v" +
to_string(uRandDist->distributionOneToN(2)) +
";\n";
return preamble + abiPragma;
set<string> pragmas = uRandDist->subset(s_genericPragmas);
// Choose either abicoder v1 or v2 but not both.
pragmas.insert(s_abiPragmas[uRandDist->distributionOneToN(s_abiPragmas.size()) - 1]);
return boost::algorithm::join(pragmas, "\n") + "\n";
}
string ImportGenerator::visit()
@ -152,24 +167,19 @@ string ImportGenerator::visit()
* Case 3: At least two source units defined
*/
ostringstream os;
// Self import with a small probability only if
// there is one source unit present in test.
if (state->size() == 1)
string importPath;
// Import a different source unit if at least
// two source units available.
if (state->size() > 1)
importPath = state->randomNonCurrentPath();
// Do not reimport already imported source unit
if (!importPath.empty() && !state->sourceUnitState[state->currentPath()]->sourcePathImported(importPath))
{
if (uRandDist->probable(s_selfImportInvProb))
os << "import "
<< "\""
<< state->randomPath()
<< "\";";
}
else
{
// Import a different source unit if at least
// two source units available.
os << "import "
<< "\""
<< state->randomNonCurrentPath()
<< "\";";
<< "\""
<< importPath
<< "\";\n";
state->sourceUnitState[state->currentPath()]->addImportedSourcePath(importPath);
}
return os.str();
}

View File

@ -68,57 +68,139 @@ struct UniformRandomDistribution
/// uniformly at random.
[[nodiscard]] size_t distributionOneToN(size_t _n) const
{
solAssert(_n > 0, "");
return Distribution(1, _n)(*randomEngine);
}
/// @returns true with a probability of 1/(@param _n), false otherwise.
/// @param _n must be non zero.
/// @param _n > 1.
[[nodiscard]] bool probable(size_t _n) const
{
solAssert(_n > 0, "");
solAssert(_n > 1, "");
return distributionOneToN(_n) == 1;
}
/// @returns true with a probability of 1 - 1/(@param _n),
/// false otherwise.
/// @param _n > 1.
[[nodiscard]] bool likely(size_t _n) const
{
solAssert(_n > 1, "");
return !probable(_n);
}
/// @returns a subset whose elements are of type @param T
/// created from the set @param _container using
/// uniform selection.
template <typename T>
std::set<T> subset(std::set<T> const& _container)
{
size_t s = _container.size();
solAssert(s > 1, "");
std::set<T> subContainer;
for (auto const& item: _container)
if (probable(s))
subContainer.insert(item);
return subContainer;
}
std::unique_ptr<RandomEngine> randomEngine;
};
struct SourceState
{
explicit SourceState(std::shared_ptr<UniformRandomDistribution> _urd):
uRandDist(std::move(_urd)),
importedSources({})
{}
void addImportedSourcePath(std::string& _sourcePath)
{
importedSources.emplace(_sourcePath);
}
[[nodiscard]] bool sourcePathImported(std::string const& _sourcePath) const
{
return importedSources.count(_sourcePath);
}
~SourceState()
{
importedSources.clear();
}
/// Prints source state to @param _os.
void print(std::ostream& _os) const;
std::shared_ptr<UniformRandomDistribution> uRandDist;
std::set<std::string> importedSources;
};
struct TestState
{
explicit TestState(std::shared_ptr<UniformRandomDistribution> _urd):
sourceUnitPaths({}),
sourceUnitState({}),
currentSourceUnitPath({}),
uRandDist(std::move(_urd))
uRandDist(std::move(_urd)),
numSourceUnits(0)
{}
/// Adds @param _path to @name sourceUnitPaths updates
/// @name currentSourceUnitPath.
void addSourceUnit(std::string const& _path)
{
sourceUnitPaths.insert(_path);
sourceUnitState.emplace(_path, std::make_shared<SourceState>(uRandDist));
currentSourceUnitPath = _path;
}
/// @returns true if @name sourceUnitPaths is empty,
/// Returns true if @name sourceUnitPaths is empty,
/// false otherwise.
[[nodiscard]] bool empty() const
{
return sourceUnitPaths.empty();
return sourceUnitState.empty();
}
/// @returns the number of items in @name sourceUnitPaths.
/// Returns the number of items in @name sourceUnitPaths.
[[nodiscard]] size_t size() const
{
return sourceUnitPaths.size();
return sourceUnitState.size();
}
/// Returns a new source path name that is formed by concatenating
/// a static prefix @name m_sourceUnitNamePrefix, a monotonically
/// increasing counter starting from 0 and the postfix (extension)
/// ".sol".
[[nodiscard]] std::string newPath() const
{
return sourceUnitNamePrefix + std::to_string(numSourceUnits) + ".sol";
}
[[nodiscard]] std::string currentPath() const
{
solAssert(numSourceUnits > 0, "");
return currentSourceUnitPath;
}
/// Adds @param _path to list of source paths in global test
/// state and increments @name m_numSourceUnits.
void updateSourcePath(std::string const& _path)
{
addSourceUnit(_path);
numSourceUnits++;
}
/// Adds a new source unit to test case.
void addSource()
{
updateSourcePath(newPath());
}
~TestState()
{
sourceUnitState.clear();
}
/// Prints test state to @param _os.
void print(std::ostream& _os) const;
/// @returns a randomly chosen path from @param _sourceUnitPaths.
/// Returns a randomly chosen path from @param _sourceUnitPaths.
[[nodiscard]] std::string randomPath(std::set<std::string> const& _sourceUnitPaths) const;
/// @returns a randomly chosen path from @name sourceUnitPaths.
[[nodiscard]] std::set<std::string> sourceUnitPaths() const;
/// Returns a randomly chosen path from @name sourceUnitPaths.
[[nodiscard]] std::string randomPath() const;
/// @returns a randomly chosen non current source unit path.
/// Returns a randomly chosen non current source unit path.
[[nodiscard]] std::string randomNonCurrentPath() const;
/// List of source paths in test input.
std::set<std::string> sourceUnitPaths;
std::map<std::string, std::shared_ptr<SourceState>> sourceUnitState;
/// Source path being currently visited.
std::string currentSourceUnitPath;
/// Uniform random distribution.
std::shared_ptr<UniformRandomDistribution> uRandDist;
/// Number of source units in test input
size_t numSourceUnits;
/// String prefix of source unit names
std::string const sourceUnitNamePrefix = "su";
};
struct GeneratorBase
@ -128,8 +210,8 @@ struct GeneratorBase
std::shared_ptr<T> generator()
{
for (auto& g: generators)
if (std::holds_alternative<std::shared_ptr<T>>(g))
return std::get<std::shared_ptr<T>>(g);
if (std::holds_alternative<std::shared_ptr<T>>(g.first))
return std::get<std::shared_ptr<T>>(g.first);
solAssert(false, "");
}
/// @returns test fragment created by this generator.
@ -151,7 +233,7 @@ struct GeneratorBase
std::string visitChildren();
/// Adds generators for child grammar elements of
/// this grammar element.
void addGenerators(std::set<GeneratorPtr> _generators)
void addGenerators(std::set<std::pair<GeneratorPtr, unsigned>> _generators)
{
generators += _generators;
}
@ -168,7 +250,7 @@ struct GeneratorBase
/// Shared pointer to the mutator instance
std::shared_ptr<SolidityGenerator> mutator;
/// Set of generators used by this generator.
std::set<GeneratorPtr> generators;
std::set<std::pair<GeneratorPtr, unsigned>> generators;
/// Shared ptr to global test state.
std::shared_ptr<TestState> state;
/// Uniform random distribution
@ -179,8 +261,7 @@ class TestCaseGenerator: public GeneratorBase
{
public:
explicit TestCaseGenerator(std::shared_ptr<SolidityGenerator> _mutator):
GeneratorBase(std::move(_mutator)),
m_numSourceUnits(0)
GeneratorBase(std::move(_mutator))
{}
void setup() override;
std::string visit() override;
@ -221,6 +302,8 @@ public:
void setup() override;
std::string visit() override;
std::string name() override { return "Source unit generator"; }
private:
static unsigned constexpr s_maxImports = 2;
};
class PragmaGenerator: public GeneratorBase
@ -231,6 +314,15 @@ public:
{}
std::string visit() override;
std::string name() override { return "Pragma generator"; }
private:
std::set<std::string> const s_genericPragmas = {
R"(pragma solidity >= 0.0.0;)",
R"(pragma experimental SMTChecker;)",
};
std::vector<std::string> const s_abiPragmas = {
R"(pragma abicoder v1;)",
R"(pragma abicoder v2;)"
};
};
class ImportGenerator: public GeneratorBase
@ -241,15 +333,6 @@ public:
{}
std::string visit() override;
std::string name() override { return "Import generator"; }
private:
/// Inverse probability with which a source unit
/// imports itself. Keeping this at 17 seems to
/// produce self imported source units with a
/// frequency small enough so that it does not
/// consume too many fuzzing cycles but large
/// enough so that the fuzzer generates self
/// import statements every once in a while.
static constexpr size_t s_selfImportInvProb = 17;
};
class SolidityGenerator: public std::enable_shared_from_this<SolidityGenerator>