blob: 6bb79b1069609cf84f3ff824be3ab6b267222633 [file] [log] [blame] [edit]
/*
* Copyright 2023 WebAssembly Community Group participants
*
* 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.
*/
#include <optional>
#include <random>
#include <string>
#include "analysis/lattice.h"
#include "analysis/liveness-transfer-function.h"
#include "analysis/reaching-definitions-transfer-function.h"
#include "analysis/stack-lattice.h"
#include "support/command-line.h"
#include "tools/fuzzing.h"
#include "tools/fuzzing/random.h"
namespace wasm {
using RandEngine = std::mt19937_64;
using namespace analysis;
// Helps printing error messages.
std::string LatticeComparisonNames[4] = {
"No Relation", "Equal", "Less", "Greater"};
std::string LatticeComparisonSymbols[4] = {"?", "=", "<", ">"};
uint64_t getSeed() {
// Return a (truly) random 64-bit value.
std::random_device rand;
return std::uniform_int_distribution<uint64_t>{}(rand);
}
// Utility class which provides methods to check properties of the transfer
// function and lattice of an analysis.
template<typename Lattice, typename TransferFunction> struct AnalysisChecker {
Lattice& lattice;
TransferFunction& transferFunction;
std::string latticeName;
std::string transferFunctionName;
uint64_t latticeElementSeed;
Name funcName;
AnalysisChecker(Lattice& lattice,
TransferFunction& transferFunction,
std::string latticeName,
std::string transferFunctionName,
uint64_t latticeElementSeed,
Name funcName)
: lattice(lattice), transferFunction(transferFunction),
latticeName(latticeName), transferFunctionName(transferFunctionName),
latticeElementSeed(latticeElementSeed), funcName(funcName) {}
void printFailureInfo(std::ostream& os) {
os << "Error for " << transferFunctionName << " and " << latticeName
<< " at lattice element seed " << latticeElementSeed << " and function "
<< funcName << ".\n";
}
// Prints information about a particular test case consisting of a randomly
// generated function and triple of randomly generate lattice elements.
void printVerboseFunctionCase(std::ostream& os,
typename Lattice::Element& x,
typename Lattice::Element& y,
typename Lattice::Element& z) {
os << "Using lattice element seed " << latticeElementSeed << "\nGenerated "
<< latticeName << " elements:\n";
x.print(os);
os << ",\n";
y.print(os);
os << ",\n";
z.print(os);
os << "\nfor " << funcName << " to test " << transferFunctionName
<< ".\n\n";
}
// Checks reflexivity of a lattice element, i.e. x = x.
void checkReflexivity(typename Lattice::Element& element) {
LatticeComparison result = lattice.compare(element, element);
if (result != LatticeComparison::EQUAL) {
std::stringstream ss;
printFailureInfo(ss);
ss << "Element ";
element.print(ss);
ss << " is not reflexive.\n";
Fatal() << ss.str();
}
}
// Anti-Symmetry is defined as x <= y and y <= x imply x = y. Due to the
// fact that the compare(x, y) function of the lattice explicitly tells
// us if two lattice elements are <, =, or = instead of providing a
// <= comparison, it is not useful to check for anti-symmetry as it is defined
// in the fuzzer.
//
// Instead, we check for a related concept that x < y implies y > x, and
// vice versa in this checkAntiSymmetry function.
void checkAntiSymmetry(typename Lattice::Element& x,
typename Lattice::Element& y) {
LatticeComparison result = lattice.compare(x, y);
LatticeComparison reverseResult = lattice.compare(y, x);
if (reverseComparison(result) != reverseResult) {
std::stringstream ss;
printFailureInfo(ss);
x.print(ss);
ss << " " << LatticeComparisonNames[result] << " ";
y.print(ss);
ss << " but reverse direction comparison is "
<< LatticeComparisonNames[reverseResult] << ".\n";
Fatal() << ss.str();
}
}
private:
// Prints the error message when a triple of lattice elements violates
// transitivity.
void printTransitivityError(std::ostream& os,
typename Lattice::Element& a,
typename Lattice::Element& b,
typename Lattice::Element& c,
LatticeComparison ab,
LatticeComparison bc,
LatticeComparison ac) {
printFailureInfo(os);
os << "Elements a = ";
a.print(os);
os << ", b = ";
b.print(os);
os << ", and c = ";
c.print(os);
os << " are not transitive. a" << LatticeComparisonSymbols[ab] << "b and b"
<< LatticeComparisonSymbols[bc] << "c, but a"
<< LatticeComparisonSymbols[ac] << "c.\n";
}
// Returns true if given a-b and b-c comparisons, the a-c comparison violates
// transitivity.
bool violatesTransitivity(LatticeComparison ab,
LatticeComparison bc,
LatticeComparison ac) {
if (ab != LatticeComparison::NO_RELATION &&
(bc == LatticeComparison::EQUAL || bc == ab) && ab != ac) {
return true;
} else if (bc != LatticeComparison::NO_RELATION &&
(ab == LatticeComparison::EQUAL || ab == bc) && bc != ac) {
return true;
}
return false;
}
public:
// Given three lattice elements x, y, and z, checks if transitivity holds
// between them.
void checkTransitivity(typename Lattice::Element& x,
typename Lattice::Element& y,
typename Lattice::Element& z) {
LatticeComparison xy = lattice.compare(x, y);
LatticeComparison yz = lattice.compare(y, z);
LatticeComparison xz = lattice.compare(x, z);
LatticeComparison yx = reverseComparison(xy);
LatticeComparison zy = reverseComparison(yz);
// Cover all permutations of x, y, and z.
if (violatesTransitivity(xy, yz, xz)) {
std::stringstream ss;
printTransitivityError(ss, x, y, z, xy, yz, xz);
Fatal() << ss.str();
} else if (violatesTransitivity(yx, xz, yz)) {
std::stringstream ss;
printTransitivityError(ss, y, x, z, yx, xz, yz);
Fatal() << ss.str();
} else if (violatesTransitivity(xz, zy, xy)) {
std::stringstream ss;
printTransitivityError(ss, x, z, y, xz, zy, xy);
Fatal() << ss.str();
}
}
// Given two input - output lattice pairs of a transfer function, checks if
// the transfer function is monotonic. If this is violated, then we print out
// the CFG block input which caused the transfer function to exhibit
// non-monotonic behavior.
void checkMonotonicity(const BasicBlock* cfgBlock,
typename Lattice::Element& first,
typename Lattice::Element& second,
typename Lattice::Element& firstResult,
typename Lattice::Element& secondResult) {
LatticeComparison beforeCmp = lattice.compare(first, second);
LatticeComparison afterCmp = lattice.compare(firstResult, secondResult);
// Cases in which monotonicity is preserved.
if (beforeCmp == LatticeComparison::NO_RELATION) {
// If there is no relation in the first place, we can't expect anything.
return;
} else if (beforeCmp == LatticeComparison::LESS &&
(afterCmp == LatticeComparison::LESS ||
afterCmp == LatticeComparison::EQUAL)) {
// x < y and f(x) <= f(y)
return;
} else if (beforeCmp == LatticeComparison::GREATER &&
(afterCmp == LatticeComparison::GREATER ||
afterCmp == LatticeComparison::EQUAL)) {
// x > y and f(x) >= f(y)
return;
} else if (beforeCmp == LatticeComparison::EQUAL &&
afterCmp == LatticeComparison::EQUAL) {
// x = y and f(x) = f(y)
return;
}
std::stringstream ss;
printFailureInfo(ss);
ss << "Elements ";
first.print(ss);
ss << " -> ";
firstResult.print(ss);
ss << " and ";
second.print(ss);
ss << " -> ";
secondResult.print(ss);
ss << "\n show that the transfer function is not monotone when given the "
"input:\n";
cfgBlock->print(ss);
ss << "\n";
Fatal() << ss.str();
}
// Checks lattice-only properties for a triple of lattices.
void checkLatticeElements(typename Lattice::Element x,
typename Lattice::Element y,
typename Lattice::Element z) {
checkReflexivity(x);
checkReflexivity(y);
checkReflexivity(z);
checkAntiSymmetry(x, y);
checkAntiSymmetry(x, z);
checkAntiSymmetry(y, z);
checkTransitivity(x, y, z);
}
// Checks transfer function relevant properties given a CFG and three input
// states. It does this by applying the transfer function on each CFG block
// using the same three input states each time and then checking properties on
// the inputs and outputs.
void checkTransferFunction(CFG& cfg,
typename Lattice::Element x,
typename Lattice::Element y,
typename Lattice::Element z) {
for (auto cfgIter = cfg.begin(); cfgIter != cfg.end(); ++cfgIter) {
// Apply transfer function on each lattice element.
typename Lattice::Element xResult = x;
transferFunction.transfer(&(*cfgIter), xResult);
typename Lattice::Element yResult = y;
transferFunction.transfer(&(*cfgIter), yResult);
typename Lattice::Element zResult = z;
transferFunction.transfer(&(*cfgIter), zResult);
// Check monotonicity for every pair of transfer function outputs.
checkMonotonicity(&(*cfgIter), x, y, xResult, yResult);
checkMonotonicity(&(*cfgIter), x, z, xResult, zResult);
checkMonotonicity(&(*cfgIter), y, z, yResult, zResult);
}
}
};
// Struct to set up and check liveness analysis lattice and transfer function.
struct LivenessChecker {
LivenessTransferFunction transferFunction;
FiniteIntPowersetLattice lattice;
AnalysisChecker<FiniteIntPowersetLattice, LivenessTransferFunction> checker;
LivenessChecker(Function* func, uint64_t latticeElementSeed, Name funcName)
: lattice(func->getNumLocals()), checker(lattice,
transferFunction,
"FiniteIntPowersetLattice",
"LivenessTransferFunction",
latticeElementSeed,
funcName) {}
FiniteIntPowersetLattice::Element getRandomElement(Random& rand) {
FiniteIntPowersetLattice::Element result = lattice.getBottom();
// Uses rand to randomly select which members are to be included (i. e. flip
// bits in the bitvector).
for (size_t i = 0; i < lattice.getSetSize(); ++i) {
result.set(i, rand.oneIn(2));
}
return result;
}
// Runs all checks for liveness analysis.
void runChecks(CFG& cfg, Random& rand, bool verbose) {
FiniteIntPowersetLattice::Element x = getRandomElement(rand);
FiniteIntPowersetLattice::Element y = getRandomElement(rand);
FiniteIntPowersetLattice::Element z = getRandomElement(rand);
if (verbose) {
checker.printVerboseFunctionCase(std::cout, x, y, z);
}
checker.checkLatticeElements(x, y, z);
checker.checkTransferFunction(cfg, x, y, z);
}
};
// Struct to set up and check reaching definitions analysis lattice and transfer
// function.
struct ReachingDefinitionsChecker {
LocalGraph::GetSetses getSetses;
LocalGraph::Locations locations;
ReachingDefinitionsTransferFunction transferFunction;
AnalysisChecker<FinitePowersetLattice<LocalSet*>,
ReachingDefinitionsTransferFunction>
checker;
ReachingDefinitionsChecker(Function* func,
uint64_t latticeElementSeed,
Name funcName)
: transferFunction(func, getSetses, locations),
checker(transferFunction.lattice,
transferFunction,
"FinitePowersetLattice<LocalSet*>",
"ReachingDefinitionsTransferFunction",
latticeElementSeed,
funcName) {}
FinitePowersetLattice<LocalSet*>::Element getRandomElement(Random& rand) {
FinitePowersetLattice<LocalSet*>::Element result =
transferFunction.lattice.getBottom();
// Uses rand to randomly select which members are to be included (i. e. flip
// bits in the bitvector).
for (size_t i = 0; i < transferFunction.lattice.getSetSize(); ++i) {
result.set(i, rand.oneIn(2));
}
return result;
}
// Runs all checks for reaching definitions analysis.
void runChecks(CFG& cfg, Random& rand, bool verbose) {
FinitePowersetLattice<LocalSet*>::Element x = getRandomElement(rand);
FinitePowersetLattice<LocalSet*>::Element y = getRandomElement(rand);
FinitePowersetLattice<LocalSet*>::Element z = getRandomElement(rand);
if (verbose) {
checker.printVerboseFunctionCase(std::cout, x, y, z);
}
checker.checkLatticeElements(x, y, z);
checker.checkTransferFunction(cfg, x, y, z);
}
};
// Struct to set up and check the stack lattice. Uses the
// FiniteIntPowersetLattice as to model stack contents.
struct StackLatticeChecker {
FiniteIntPowersetLattice contentLattice;
StackLattice<FiniteIntPowersetLattice> stackLattice;
// Here only as a placeholder, since the checker utility currently wants a
// transfer function.
LivenessTransferFunction transferFunction;
AnalysisChecker<StackLattice<FiniteIntPowersetLattice>,
LivenessTransferFunction>
checker;
StackLatticeChecker(Function* func,
uint64_t latticeElementSeed,
Name funcName)
: contentLattice(func->getNumLocals()), stackLattice(contentLattice),
checker(stackLattice,
transferFunction,
"StackLattice<FiniteIntPowersetLattice>",
"LivenessTransferFunction",
latticeElementSeed,
funcName) {}
StackLattice<FiniteIntPowersetLattice>::Element
getRandomElement(Random& rand) {
StackLattice<FiniteIntPowersetLattice>::Element result =
stackLattice.getBottom();
// Randomly decide on a stack height. A max height of 15 stack elements is
// arbitrarily chosen.
size_t stackHeight = rand.upTo(15);
for (size_t j = 0; j < stackHeight; ++j) {
// Randomly generate a FiniteIntPowersetLattice and push it onto the
// stack.
FiniteIntPowersetLattice::Element content = contentLattice.getBottom();
for (size_t i = 0; i < contentLattice.getSetSize(); ++i) {
content.set(i, rand.oneIn(2));
}
result.push(std::move(content));
}
return result;
}
// Runs all checks for the StackLattice analysis.
void runChecks(Random& rand, bool verbose) {
StackLattice<FiniteIntPowersetLattice>::Element x = getRandomElement(rand);
StackLattice<FiniteIntPowersetLattice>::Element y = getRandomElement(rand);
StackLattice<FiniteIntPowersetLattice>::Element z = getRandomElement(rand);
if (verbose) {
checker.printVerboseFunctionCase(std::cout, x, y, z);
}
checker.checkLatticeElements(x, y, z);
}
};
struct Fuzzer {
bool verbose;
Fuzzer(bool verbose) : verbose(verbose) {}
// Helper function to run per-function tests. latticeElementSeed is used to
// generate three lattice elements randomly. It is also used to select which
// analysis is to be tested for the function.
void runOnFunction(Function* func, uint64_t latticeElementSeed) {
RandEngine getFuncRand(latticeElementSeed);
// Fewer bytes are needed to generate three random lattices.
std::vector<char> funcBytes(128);
for (size_t i = 0; i < funcBytes.size(); i += sizeof(uint64_t)) {
*(uint64_t*)(funcBytes.data() + i) = getFuncRand();
}
Random rand(std::move(funcBytes));
CFG cfg = CFG::fromFunction(func);
switch (rand.upTo(3)) {
case 0: {
LivenessChecker livenessChecker(func, latticeElementSeed, func->name);
livenessChecker.runChecks(cfg, rand, verbose);
break;
}
case 1: {
ReachingDefinitionsChecker reachingDefinitionsChecker(
func, latticeElementSeed, func->name);
reachingDefinitionsChecker.runChecks(cfg, rand, verbose);
break;
}
default: {
StackLatticeChecker stackLatticeChecker(
func, latticeElementSeed, func->name);
stackLatticeChecker.runChecks(rand, verbose);
}
}
}
// Generates a module. The module is used as an input to fuzz transfer
// functions as well as randomly generated lattice element states. Lattice
// properties are also fuzzed from the randomly generated states.
void run(uint64_t seed,
uint64_t* latticeElementSeed = nullptr,
std::string* funcName = nullptr) {
RandEngine getRand(seed);
std::cout << "Running with seed " << seed << "\n";
// 4kb of random bytes should be enough for anyone!
std::vector<char> bytes(4096);
for (size_t i = 0; i < bytes.size(); i += sizeof(uint64_t)) {
*(uint64_t*)(bytes.data() + i) = getRand();
}
Module testModule;
TranslateToFuzzReader reader(testModule, std::move(bytes));
reader.build();
if (verbose) {
std::cout << "Generated test module: \n";
std::cout << testModule;
std::cout << "\n";
}
// If a specific function and lattice element seed is specified, only run
// that.
if (latticeElementSeed && funcName) {
runOnFunction(testModule.getFunction(*funcName), *latticeElementSeed);
return;
}
ModuleUtils::iterDefinedFunctions(testModule, [&](Function* func) {
uint64_t funcSeed = getRand();
runOnFunction(func, funcSeed);
});
}
};
} // namespace wasm
int main(int argc, const char* argv[]) {
using namespace wasm;
const std::string WasmFuzzTypesOption = "wasm-fuzz-lattices options";
Options options("wasm-fuzz-lattices",
"Fuzz lattices for reflexivity, transitivity, and "
"anti-symmetry, and tranfer functions for monotonicity.");
std::optional<uint64_t> seed;
options.add("--seed",
"",
"Run a single workload generated by the given seed",
WasmFuzzTypesOption,
Options::Arguments::One,
[&](Options*, const std::string& arg) {
seed = uint64_t(std::stoull(arg));
});
std::optional<uint64_t> latticeElementSeed;
options.add("--lattice-element-seed",
"",
"Seed which generated the lattice elements to be checked.",
WasmFuzzTypesOption,
Options::Arguments::One,
[&](Options*, const std::string& arg) {
latticeElementSeed = uint64_t(std::stoull(arg));
});
std::optional<std::string> functionName;
options.add(
"--function-name",
"",
"Name of the function in the module generated by --seed to be checked.",
WasmFuzzTypesOption,
Options::Arguments::One,
[&](Options*, const std::string& arg) { functionName = arg; });
bool verbose = false;
options.add("--verbose",
"-v",
"Print extra information",
WasmFuzzTypesOption,
Options::Arguments::Zero,
[&](Options*, const std::string& arg) { verbose = true; });
options.parse(argc, argv);
Fuzzer fuzzer{verbose};
if (seed) {
if (latticeElementSeed && functionName) {
// Run test a single function and lattice element seed.
fuzzer.run(*seed, &(*latticeElementSeed), &(*functionName));
} else {
// Run just a single workload with the given seed.
fuzzer.run(*seed);
}
} else {
// Continuously run workloads with new randomly generated seeds.
size_t i = 0;
RandEngine nextSeed(getSeed());
while (true) {
std::cout << "Iteration " << ++i << "\n";
fuzzer.run(nextSeed());
}
}
return 0;
}