diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ef9ea07e..49f78fa19 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,6 +50,7 @@ defaults: cd build protoc --proto_path=../test/tools/ossfuzz yulProto.proto --cpp_out=../test/tools/ossfuzz protoc --proto_path=../test/tools/ossfuzz abiV2Proto.proto --cpp_out=../test/tools/ossfuzz + protoc --proto_path=../test/tools/ossfuzz solProto.proto --cpp_out=../test/tools/ossfuzz cmake .. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} $CMAKE_OPTIONS make ossfuzz ossfuzz_proto ossfuzz_abiv2 -j4 @@ -97,6 +98,7 @@ defaults: - test/tools/ossfuzz/strictasm_opt_ossfuzz - test/tools/ossfuzz/yul_proto_diff_ossfuzz - test/tools/ossfuzz/yul_proto_ossfuzz + - test/tools/ossfuzz/sol_proto_ossfuzz # test result output directory - artifacts_test_results: &artifacts_test_results diff --git a/test/tools/ossfuzz/CMakeLists.txt b/test/tools/ossfuzz/CMakeLists.txt index 83543b9f0..ccd71a983 100644 --- a/test/tools/ossfuzz/CMakeLists.txt +++ b/test/tools/ossfuzz/CMakeLists.txt @@ -10,7 +10,7 @@ add_dependencies(ossfuzz if (OSSFUZZ) add_custom_target(ossfuzz_proto) - add_dependencies(ossfuzz_proto yul_proto_ossfuzz yul_proto_diff_ossfuzz) + add_dependencies(ossfuzz_proto yul_proto_ossfuzz yul_proto_diff_ossfuzz sol_proto_ossfuzz) add_custom_target(ossfuzz_abiv2) add_dependencies(ossfuzz_abiv2 abiv2_proto_ossfuzz) @@ -78,6 +78,25 @@ if (OSSFUZZ) protobuf.a ) set_target_properties(abiv2_proto_ossfuzz PROPERTIES LINK_FLAGS ${LIB_FUZZING_ENGINE}) + + add_executable(sol_proto_ossfuzz + solProtoFuzzer.cpp + protoToSol.cpp + solProto.pb.cc + abiV2FuzzerCommon.cpp + ../../EVMHost.cpp + ) + target_include_directories(sol_proto_ossfuzz PRIVATE + /usr/include/libprotobuf-mutator + ) + target_link_libraries(sol_proto_ossfuzz PRIVATE solidity libsolc + evmc + evmone-standalone + protobuf-mutator-libfuzzer.a + protobuf-mutator.a + protobuf.a + ) + set_target_properties(sol_proto_ossfuzz PROPERTIES LINK_FLAGS ${LIB_FUZZING_ENGINE}) else() add_library(solc_opt_ossfuzz solc_opt_ossfuzz.cpp diff --git a/test/tools/ossfuzz/protoToSol.cpp b/test/tools/ossfuzz/protoToSol.cpp new file mode 100644 index 000000000..aefbb41b9 --- /dev/null +++ b/test/tools/ossfuzz/protoToSol.cpp @@ -0,0 +1,228 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#include + +#include + +#include + +#include + +using namespace solidity::test::solprotofuzzer; +using namespace solidity::util; +using namespace std; + +string ProtoConverter::protoToSolidity(Program const& _p) +{ + // Create random number generator with fuzzer supplied + // seed. + m_randomGen = make_shared(_p.seed()); + return visit(_p); +} + +pair ProtoConverter::generateTestCase(TestContract const& _testContract) +{ + ostringstream testCode; + string usingLibDecl; + switch (_testContract.type()) + { + case TestContract::LIBRARY: + { + m_libraryTest = true; + auto testTuple = pseudoRandomLibraryTest(); + m_libraryName = get<0>(testTuple); + Whiskers u(R"(using for uint;)"); + u("ind", "\t"); + u("libraryName", get<0>(testTuple)); + usingLibDecl = u.render(); + Whiskers test(R"()"); + test("endl", "\n"); + test("ind", "\t\t"); + test("varDecl", "uint x;"); + Whiskers ifStmt(R"(if ()return 1;)"); + Whiskers ifCond(R"(x.() != )"); + ifCond("testFunction", get<1>(testTuple)); + ifCond("expectedOutput", get<2>(testTuple)); + ifStmt("cond", ifCond.render()); + ifStmt("endl", "\n"); + ifStmt("ind", "\t\t\t"); + test("ifStmt", ifStmt.render()); + break; + } + case TestContract::CONTRACT: + { + unsigned errorCode = 1; + unsigned contractVarIndex = 0; + for (auto const& testTuple: m_contractTests) + { + // Do this to avoid stack too deep errors + // We require uint as a return var, so we + // cannot have more than 16 variables without + // running into stack too deep errors + if (contractVarIndex >= s_maxVars) + break; + string contractName = testTuple.first; + string contractVarName = "tc" + to_string(contractVarIndex); + Whiskers init(R"( = new ();)"); + init("endl", "\n"); + init("ind", "\t\t"); + init("contractName", contractName); + init("contractVarName", contractVarName); + testCode << init.render(); + for (auto const& t: testTuple.second) + { + Whiskers tc(R"()"); + tc("endl", "\n"); + tc("ind", "\t\t"); + Whiskers ifStmt(R"(if ()return ;)"); + Whiskers ifCond(R"(.() != )"); + ifCond("contractVarName", contractVarName); + ifCond("testFunction", t.first); + ifCond("expectedOutput", t.second); + ifStmt("endl", "\n"); + ifStmt("cond", ifCond.render()); + ifStmt("ind", "\t\t\t"); + ifStmt("errorCode", to_string(errorCode)); + tc("ifStmt", ifStmt.render()); + testCode << tc.render(); + errorCode++; + } + contractVarIndex++; + } + break; + } + } + // Expected return value when all tests pass + testCode << Whiskers(R"(return 0;)")("endl", "\n")("ind", "\t\t").render(); + return {usingLibDecl, testCode.str()}; +} + +string ProtoConverter::visit(TestContract const& _testContract) +{ + string testCode; + string usingLibDecl; + m_libraryTest = false; + + // Simply return valid uint (zero) if there are + // no tests. + if (emptyLibraryTests() || emptyContractTests()) + testCode = Whiskers(R"(return 0;)")("endl", "\n")("ind", "\t\t").render(); + else + tie(usingLibDecl, testCode) = generateTestCase(_testContract); + + Whiskers c(R"(contract C {})"); + c("endl", "\n"); + c("isLibrary", m_libraryTest); + c("usingDecl", usingLibDecl); + Whiskers f("function test() public returns (uint){}"); + f("ind", "\t"); + f("endl", "\n"); + f("testCode", testCode); + c("function", f.render()); + return c.render(); +} + +bool ProtoConverter::libraryTest() const +{ + return m_libraryTest; +} + +string ProtoConverter::libraryName() const +{ + return m_libraryName; +} + +string ProtoConverter::visit(Program const& _p) +{ + ostringstream program; + ostringstream contracts; + + for (auto &contract: _p.contracts()) + contracts << visit(contract); + + Whiskers p(R"(pragma solidity >=0.0;)"); + p("endl", "\n"); + p("contracts", contracts.str()); + p("test", visit(_p.test())); + return p.render(); +} + +string ProtoConverter::visit(ContractType const& _contractType) +{ + switch (_contractType.contract_type_oneof_case()) + { + case ContractType::kC: + return visit(_contractType.c()); + case ContractType::kL: + return visit(_contractType.l()); + case ContractType::kI: + return visit(_contractType.i()); + case ContractType::CONTRACT_TYPE_ONEOF_NOT_SET: + return ""; + } +} + +string ProtoConverter::visit(Contract const& _contract) +{ + openProgramScope(&_contract); + return ""; +} + +string ProtoConverter::visit(Interface const& _interface) +{ + openProgramScope(&_interface); + return ""; +} + +string ProtoConverter::visit(Library const& _library) +{ + openProgramScope(&_library); + return ""; +} + +tuple ProtoConverter::pseudoRandomLibraryTest() +{ + solAssert(m_libraryTests.size() > 0, "Sol proto fuzzer: No library tests found"); + unsigned index = randomNumber() % m_libraryTests.size(); + return m_libraryTests[index]; +} + +void ProtoConverter::openProgramScope(CIL _program) +{ + string programNamePrefix; + if (holds_alternative(_program)) + programNamePrefix = "C"; + else if (holds_alternative(_program)) + programNamePrefix = "I"; + else + programNamePrefix = "L"; + string programName = programNamePrefix + to_string(m_programNumericSuffix++); + m_programNameMap.emplace(_program, programName); +} + +string ProtoConverter::programName(CIL _program) +{ + solAssert(m_programNameMap.count(_program), "Sol proto fuzzer: Unregistered program"); + return m_programNameMap[_program]; +} + +unsigned ProtoConverter::randomNumber() +{ + solAssert(m_randomGen, "Sol proto fuzzer: Uninitialized random number generator"); + return m_randomGen->operator()(); +} diff --git a/test/tools/ossfuzz/protoToSol.h b/test/tools/ossfuzz/protoToSol.h new file mode 100644 index 000000000..de2220752 --- /dev/null +++ b/test/tools/ossfuzz/protoToSol.h @@ -0,0 +1,185 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +#pragma once + +#include + +#include +#include +#include +#include + +namespace solidity::test::solprotofuzzer +{ +/// Random number generator that is seeded with a fuzzer +/// supplied unsigned integer. +struct SolRandomNumGenerator +{ + using RandomEngine = std::minstd_rand; + + explicit SolRandomNumGenerator(unsigned _seed): m_random(RandomEngine(_seed)) {} + + /// @returns a pseudo random unsigned integer + unsigned operator()() + { + return m_random(); + } + + RandomEngine m_random; +}; + +/* There are two types of tests created by the converter: + * - library test + * - contract test + * + * The template for library test is the following: + * + * // Library generated from fuzzer protobuf specification + * library L0 { + * function f0(uint) public view returns (uint) { + * return 31337; + * } + * function f1(uint) public pure returns (uint) { + * return 455; + * } + * } + * library L1 { + * function f0(uint) external view returns (uint) { + * return 607; + * } + * } + * + * // Test entry point + * contract C { + * // Uses a single pseudo randomly chosen library + * // and calls a pseudo randomly chosen function + * // returning a non-zero error code on failure or + * // a zero uint when test passes. + * using L0 for uint; + * function test() public pure returns (uint) { + * uint x; + * if (x.f1() != 455) + * return 1; + * return 0; + * } + * } + * + * The template for contract test is the following + * // Contracts generated from fuzzer protobuf specification + * contract C0B { + * function f0() public pure virtual returns (uint) + * { + * return 42; + * } + * } + * contract C0 is C0B { + * function f0() public pure override returns (uint) + * { + * return 1337; + * } + * } + * + * // Test entry point + * contract C { + * // Invokes one or more contract functions returning + * // a non-zero error code for failure, a zero uint + * // when all tests pass + * function test() public pure returns (uint) + * { + * C0 tc0 = new C0(); + * if (tc0.f0() != 1337) + * return 1; + * C0B tc1 = new C0B(); + * if (tc1.f0() != 42) + * return 2; + * // Expected return value if all tests pass + * return 0; + * } + * } + */ +class ProtoConverter +{ +public: + ProtoConverter() {} + ProtoConverter(ProtoConverter const&) = delete; + ProtoConverter(ProtoConverter&&) = delete; + std::string protoToSolidity(Program const&); + /// @returns true if test calls a library function, false + /// otherwise + bool libraryTest() const; + /// @returns name of the library under test + std::string libraryName() const; +private: + /// Variant type that points to one of contract, interface, library protobuf messages + using CIL = std::variant; + /// Protobuf message visitors that accept a const reference to a protobuf message + /// type and return its solidity translation. + std::string visit(Program const&); + std::string visit(TestContract const&); + std::string visit(ContractType const&); + std::string visit(Interface const& _interface); + std::string visit(Library const& _library); + std::string visit(Contract const& _contract); + /// @returns a string pair containing a library declaration (relevant for library + /// tests only) and a solidity test case + std::pair generateTestCase(TestContract const& _testContract); + /// @returns name of a program i.e., contract, library or interface + std::string programName(CIL _program); + /// @returns a tuple containing the names of the library and function under + /// test, and its expected output. + std::tuple pseudoRandomLibraryTest(); + /// Performs bookkeeping for a fuzzer-supplied program + void openProgramScope(CIL _program); + /// @returns a deterministic pseudo random unsigned integer + unsigned randomNumber(); + /// @returns true if fuzzer supplied Library protobuf message + /// contains zero functions, false otherwise. + static bool emptyLibrary(Library const& _library) + { + return _library.funcdef_size() == 0; + } + /// @returns true if there are no valid library test cases, false + /// otherwise. + bool emptyLibraryTests() + { + return m_libraryTests.empty(); + } + /// @returns true if there are no valid contract test cases, false + /// otherwise. + bool emptyContractTests() + { + return m_contractTests.empty(); + } + /// Numeric suffix that is part of program names e.g., "0" in "C0" + unsigned m_programNumericSuffix = 0; + /// Flag that states whether library call is tested (true) or not (false). + bool m_libraryTest = false; + /// A smart pointer to fuzzer driven random number generator + std::shared_ptr m_randomGen; + /// Maps protobuf program to its string name + std::map m_programNameMap; + /// List of tuples containing library name, function and its expected output + std::vector> m_libraryTests; + /// Maps contract name to a map of function names and their expected output + std::map> m_contractTests; + /// Name of the library under test, relevant if m_libraryTest is set + std::string m_libraryName; + /// Maximum number of local variables in test function to avoid stack too deep + /// errors + static unsigned constexpr s_maxVars = 15; +}; +} diff --git a/test/tools/ossfuzz/solProto.proto b/test/tools/ossfuzz/solProto.proto new file mode 100644 index 000000000..86e938c7c --- /dev/null +++ b/test/tools/ossfuzz/solProto.proto @@ -0,0 +1,111 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +syntax = "proto2"; + +message InterfaceFunction { + enum StateMutability { + PURE = 0; + VIEW = 1; + PAYABLE = 2; + NONPAYABLE = 3; + } + required StateMutability mut = 1; +} + +message LibraryFunction { + // Library functions cannot be payable + enum StateMutability { + PURE = 0; + VIEW = 1; + NONPAYABLE = 2; + } + enum Visibility { + PUBLIC = 0; + EXTERNAL = 1; + INTERNAL = 2; + PRIVATE = 3; + } + required Visibility vis = 1; + required StateMutability mut = 2; +} + +message ContractFunction { + enum StateMutability { + PURE = 0; + VIEW = 1; + PAYABLE = 2; + NONPAYABLE = 3; + } + enum Visibility { + PUBLIC = 0; + EXTERNAL = 1; + INTERNAL = 2; + PRIVATE = 3; + } + required Visibility vis = 1; + required StateMutability mut = 2; + required bool virtualfunc = 3; +} + +message Library { + repeated LibraryFunction funcdef = 1; +} + +message Interface { + repeated InterfaceFunction funcdef = 1; + repeated Interface bases = 2; +} + +message Contract { + repeated ContractFunction funcdef = 1; + required bool abstract = 2; + repeated ContractOrInterface bases = 3; +} + +message ContractOrInterface { + oneof contract_or_interface_oneof { + Contract c = 1; + Interface i = 2; + } +} + +message ContractType { + oneof contract_type_oneof { + Contract c = 1; + Library l = 2; + Interface i = 3; + } +} + +message TestContract { + enum Type { + LIBRARY = 0; + CONTRACT = 1; + } + required Type type = 1; +} + +message Program { + repeated ContractType contracts = 1; + required TestContract test = 2; + // Seed is an unsigned integer that initializes + // a pseudo random number generator. + required uint64 seed = 3; +} + +package solidity.test.solprotofuzzer; diff --git a/test/tools/ossfuzz/solProtoFuzzer.cpp b/test/tools/ossfuzz/solProtoFuzzer.cpp new file mode 100644 index 000000000..80facb3e1 --- /dev/null +++ b/test/tools/ossfuzz/solProtoFuzzer.cpp @@ -0,0 +1,234 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ + +#include +#include +#include +#include + +#include +#include + +#include + +static evmc::VM evmone = evmc::VM{evmc_create_evmone()}; + +using namespace solidity::test::abiv2fuzzer; +using namespace solidity::test::solprotofuzzer; +using namespace solidity; +using namespace solidity::test; +using namespace solidity::util; +using namespace std; + +namespace +{ +/// Test function returns a uint256 value +static size_t const expectedOutputLength = 32; +/// Expected output value is decimal 0 +static uint8_t const expectedOutput[expectedOutputLength] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/// Compares the contents of the memory address pointed to +/// by `_result` of `_length` bytes to the expected output. +/// Returns true if `_result` matches expected output, false +/// otherwise. +bool isOutputExpected(evmc::result const& _run) +{ + if (_run.output_size != expectedOutputLength) + return false; + + return memcmp(_run.output_data, expectedOutput, expectedOutputLength) == 0; +} + +/// Accepts a reference to a user-specified input and returns an +/// evmc_message with all of its fields zero initialized except +/// gas and input fields. +/// The gas field is set to the maximum permissible value so that we +/// don't run into out of gas errors. The input field is copied from +/// user input. +evmc_message initializeMessage(bytes const& _input) +{ + // Zero initialize all message fields + evmc_message msg = {}; + // Gas available (value of type int64_t) is set to its maximum + // value. + msg.gas = std::numeric_limits::max(); + msg.input_data = _input.data(); + msg.input_size = _input.size(); + return msg; +} + +/// Accepts host context implementation, and keccak256 hash of the function +/// to be called at a specified address in the simulated blockchain as +/// input and returns the result of the execution of the called function. +evmc::result executeContract( + EVMHost& _hostContext, + bytes const& _functionHash, + evmc_address _deployedAddress +) +{ + evmc_message message = initializeMessage(_functionHash); + message.destination = _deployedAddress; + message.kind = EVMC_CALL; + return _hostContext.call(message); +} + +/// Accepts a reference to host context implementation and byte code +/// as input and deploys it on the simulated blockchain. Returns the +/// result of deployment. +evmc::result deployContract(EVMHost& _hostContext, bytes const& _code) +{ + evmc_message message = initializeMessage(_code); + message.kind = EVMC_CREATE; + return _hostContext.call(message); +} + +std::pair compileContract( + std::string _sourceCode, + std::string _contractName, + std::map const& _libraryAddresses = {}, + frontend::OptimiserSettings _optimization = frontend::OptimiserSettings::minimal() +) +{ + try + { + // Compile contract generated by the proto fuzzer + SolidityCompilationFramework solCompilationFramework; + return std::make_pair( + solCompilationFramework.compileContract(_sourceCode, _contractName, _libraryAddresses, _optimization), + solCompilationFramework.getMethodIdentifiers() + ); + } + // Ignore stack too deep errors during compilation + catch (evmasm::StackTooDeepException const&) + { + return std::make_pair(bytes{}, Json::Value(0)); + } +} + +evmc::result deployAndExecute(EVMHost& _hostContext, bytes _byteCode, std::string _hexEncodedInput) +{ + // Deploy contract and signal failure if deploy failed + evmc::result createResult = deployContract(_hostContext, _byteCode); + solAssert( + createResult.status_code == EVMC_SUCCESS, + "Proto solc fuzzer: Contract creation failed" + ); + + // Execute test function and signal failure if EVM reverted or + // did not return expected output on successful execution. + evmc::result callResult = executeContract( + _hostContext, + fromHex(_hexEncodedInput), + createResult.create_address + ); + + // We don't care about EVM One failures other than EVMC_REVERT + solAssert(callResult.status_code != EVMC_REVERT, "Proto solc fuzzer: EVM One reverted"); + return callResult; +} + +evmc::result compileDeployAndExecute( + std::string _sourceCode, + std::string _contractName, + std::string _methodName, + frontend::OptimiserSettings _optimization, + std::string _libraryName = {} +) +{ + bytes libraryBytecode; + Json::Value libIds; + // We target the default EVM which is the latest + langutil::EVMVersion version = {}; + EVMHost hostContext(version, evmone); + std::map _libraryAddressMap; + + // First deploy library + if (!_libraryName.empty()) + { + tie(libraryBytecode, libIds) = compileContract( + _sourceCode, + _libraryName, + {}, + _optimization + ); + // Deploy contract and signal failure if deploy failed + evmc::result createResult = deployContract(hostContext, libraryBytecode); + solAssert( + createResult.status_code == EVMC_SUCCESS, + "Proto solc fuzzer: Library deployment failed" + ); + _libraryAddressMap[_libraryName] = EVMHost::convertFromEVMC(createResult.create_address); + } + + auto [bytecode, ids] = compileContract( + _sourceCode, + _contractName, + _libraryAddressMap, + _optimization + ); + + return deployAndExecute( + hostContext, + bytecode, + ids[_methodName].asString() + ); +} +} + +DEFINE_PROTO_FUZZER(Program const& _input) +{ + ProtoConverter converter; + string sol_source = converter.protoToSolidity(_input); + + if (char const* dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) + { + // With libFuzzer binary run this to generate a YUL source file x.yul: + // PROTO_FUZZER_DUMP_PATH=x.yul ./a.out proto-input + ofstream of(dump_path); + of.write(sol_source.data(), sol_source.size()); + } + + if (char const* dump_path = getenv("SOL_DEBUG_FILE")) + { + sol_source.clear(); + // With libFuzzer binary run this to generate a YUL source file x.yul: + // PROTO_FUZZER_LOAD_PATH=x.yul ./a.out proto-input + ifstream ifstr(dump_path); + sol_source = { + std::istreambuf_iterator(ifstr), + std::istreambuf_iterator() + }; + std::cout << sol_source << std::endl; + } + + auto minimalResult = compileDeployAndExecute( + sol_source, + ":C", + "test()", + frontend::OptimiserSettings::minimal(), + converter.libraryTest() ? converter.libraryName() : "" + ); + bool successState = minimalResult.status_code == EVMC_SUCCESS; + if (successState) + solAssert( + isOutputExpected(minimalResult), + "Proto solc fuzzer: Output incorrect" + ); +}