diff --git a/test/EVMHost.cpp b/test/EVMHost.cpp index 08c827f6c..86b12e037 100644 --- a/test/EVMHost.cpp +++ b/test/EVMHost.cpp @@ -790,11 +790,19 @@ string EVMHostPrinter::state() return m_stateStream.str(); } +string EVMHostPrinter::storageOnly() +{ + if (m_host.account_exists(m_account)) + storage(); + return m_stateStream.str(); +} + void EVMHostPrinter::storage() { for (auto const& [slot, value]: m_host.get_address_storage(m_account)) if (m_host.get_storage(m_account, slot)) - m_stateStream << m_host.convertFromEVMC(slot) + m_stateStream << " " + << m_host.convertFromEVMC(slot) << ": " << m_host.convertFromEVMC(value.value) << endl; diff --git a/test/EVMHost.h b/test/EVMHost.h index c258c6dd0..9aad936c4 100644 --- a/test/EVMHost.h +++ b/test/EVMHost.h @@ -123,6 +123,8 @@ public: {} /// @returns state at account maintained by host. std::string state(); + /// @returns storage at account maintained by host. + std::string storageOnly(); private: /// Outputs storage at account to stateStream. void storage(); diff --git a/test/tools/ossfuzz/CMakeLists.txt b/test/tools/ossfuzz/CMakeLists.txt index 265533397..cdc412163 100644 --- a/test/tools/ossfuzz/CMakeLists.txt +++ b/test/tools/ossfuzz/CMakeLists.txt @@ -16,6 +16,7 @@ if (OSSFUZZ) yul_proto_diff_ossfuzz yul_proto_diff_custom_mutate_ossfuzz stack_reuse_codegen_ossfuzz + yul_evm_diff_ossfuzz ) add_custom_target(ossfuzz_abiv2) @@ -132,6 +133,38 @@ if (OSSFUZZ) -Wno-zero-length-array -Wno-suggest-destructor-override ) + + add_executable(yul_evm_diff_ossfuzz + YulToEvmDifferentialFuzzer.cpp + ../../libyul/YulOptimizerTestCommon.cpp + protoToYul.cpp + yulProto.pb.cc + ../../EVMHost.cpp + YulEvmoneInterface.cpp + yulFuzzerCommon.cpp + ) + target_include_directories(yul_evm_diff_ossfuzz PRIVATE + /usr/include/libprotobuf-mutator + ) + target_link_libraries(yul_evm_diff_ossfuzz PRIVATE + yul + yulInterpreter + evmc + evmone-standalone + protobuf-mutator-libfuzzer.a + protobuf-mutator.a + protobuf.a + ) + set_target_properties(yul_evm_diff_ossfuzz PROPERTIES LINK_FLAGS ${LIB_FUZZING_ENGINE}) + target_compile_options(yul_evm_diff_ossfuzz PUBLIC + ${COMPILE_OPTIONS} + -Wno-sign-conversion + -Wno-inconsistent-missing-destructor-override + -Wno-unused-parameter + -Wno-zero-length-array + -Wno-suggest-destructor-override + ) + add_executable(abiv2_proto_ossfuzz ../../EVMHost.cpp diff --git a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp index 89acd2c09..410541a9d 100644 --- a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp +++ b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp @@ -59,25 +59,41 @@ DEFINE_PROTO_FUZZER(Program const& _input) filterStatefulInstructions, filterUnboundedLoops ); - string yul_source = converter.programToString(_input); + string yulSubObject = converter.programToString(_input); // Fuzzer also fuzzes the EVM version field. langutil::EVMVersion version = converter.version(); EVMHost hostContext(version, evmone); hostContext.reset(); + // Do not proceed with tests that are too large. 1200 is an arbitrary + // threshold. + if (yulSubObject.size() > 1200) + return; + + YulStringRepository::reset(); + + // Package test case into a sub-object + solidity::util::Whiskers yulObjectFormat(R"( +object "main" { + code { + codecopy(0, dataoffset("deployed"), datasize("deployed")) + return(0, datasize("deployed")) + } + object "deployed" { + code { + + } + } +} + )"); + string yul_source = yulObjectFormat("fuzzerInput", yulSubObject).render(); + if (const char* dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) { ofstream of(dump_path); of.write(yul_source.data(), static_cast(yul_source.size())); } - // Do not proceed with tests that are too large. 1200 is an arbitrary - // threshold. - if (yul_source.size() > 1200) - return; - - YulStringRepository::reset(); - solidity::frontend::OptimiserSettings settings = solidity::frontend::OptimiserSettings::full(); settings.runYulOptimiser = false; settings.optimizeStackAllocation = false; @@ -93,7 +109,10 @@ DEFINE_PROTO_FUZZER(Program const& _input) evmc::result deployResult = YulEvmoneUtility{}.deployCode(unoptimisedByteCode, hostContext); if (deployResult.status_code != EVMC_SUCCESS) + { + cout << "Deploy unoptimised failed." << endl; return; + } auto callMessage = YulEvmoneUtility{}.callMessage(deployResult.create_address); evmc::result callResult = hostContext.call(callMessage); // If the fuzzer synthesized input does not contain the revert opcode which @@ -112,13 +131,17 @@ DEFINE_PROTO_FUZZER(Program const& _input) ); // Bail out on serious errors encountered during a call. if (YulEvmoneUtility{}.seriousCallError(callResult.status_code)) + { + cout << "Unoptimised call failed." << endl; return; + } solAssert( (callResult.status_code == EVMC_SUCCESS || (!noRevertInSource && callResult.status_code == EVMC_REVERT) || (!noInvalidInSource && callResult.status_code == EVMC_INVALID_INSTRUCTION)), "Unoptimised call failed." ); + ostringstream unoptimizedState; unoptimizedState << EVMHostPrinter{hostContext, deployResult.create_address}.state(); @@ -162,18 +185,11 @@ DEFINE_PROTO_FUZZER(Program const& _input) ostringstream optimizedState; optimizedState << EVMHostPrinter{hostContext, deployResultOpt.create_address}.state(); - int64_t constexpr tolerance = 1000; - if (callResult.gas_left > callResultOpt.gas_left) - if (callResult.gas_left - callResultOpt.gas_left > tolerance) - { - cout << "Gas differential " << callResult.gas_left - callResultOpt.gas_left << endl; - cout << "Unoptimised bytecode" << endl; - cout << util::toHex(unoptimisedByteCode) << endl; - cout << "Optimised bytecode" << endl; - cout << util::toHex(optimisedByteCode) << endl; - solAssert(false, "Optimised code consumed more than +1000 gas."); - } - + if (unoptimizedState.str() != optimizedState.str()) + { + cout << unoptimizedState.str() << endl; + cout << optimizedState.str() << endl; + } solAssert( unoptimizedState.str() == optimizedState.str(), "State of unoptimised and optimised stack reused code do not match." diff --git a/test/tools/ossfuzz/YulEvmoneInterface.cpp b/test/tools/ossfuzz/YulEvmoneInterface.cpp index fe737993d..a4f095d8e 100644 --- a/test/tools/ossfuzz/YulEvmoneInterface.cpp +++ b/test/tools/ossfuzz/YulEvmoneInterface.cpp @@ -48,24 +48,8 @@ evmc::result YulEvmoneUtility::deployCode(bytes const& _input, EVMHost& _host) _input.size() <= 0xffff, "Deployed byte code is larger than the permissible 65535 bytes." ); - uint8_t inputSizeHigher = static_cast(_input.size() >> 8); - uint8_t inputSizeLower = _input.size() & 0xff; - /* - * CODESIZE - * PUSH0 0xc - * PUSH0 0x0 - * CODECOPY - * PUSH1 - * PUSH0 0x0 - * RETURN - */ - bytes deployCode = bytes{ - 0x38, 0x60, 0x0c, 0x60, 0x00, 0x39, 0x61, - inputSizeHigher, inputSizeLower, - 0x60, 0x00, 0xf3 - } + _input; - msg.input_data = deployCode.data(); - msg.input_size = deployCode.size(); + msg.input_data = _input.data(); + msg.input_size = _input.size(); msg.kind = EVMC_CREATE; return _host.call(msg); } diff --git a/test/tools/ossfuzz/YulToEvmDifferentialFuzzer.cpp b/test/tools/ossfuzz/YulToEvmDifferentialFuzzer.cpp new file mode 100644 index 000000000..de94e56a5 --- /dev/null +++ b/test/tools/ossfuzz/YulToEvmDifferentialFuzzer.cpp @@ -0,0 +1,193 @@ +/* + 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 + +#include +#include + +#include + +#include + +#include + +#include + +#include + +#include + +using namespace solidity; +using namespace solidity::test; +using namespace solidity::test::fuzzer; +using namespace solidity::yul; +using namespace solidity::yul::test; +using namespace solidity::yul::test::yul_fuzzer; +using namespace solidity::langutil; +using namespace solidity::util; +using namespace std; + +static evmc::VM evmone = evmc::VM{evmc_create_evmone()}; + +DEFINE_PROTO_FUZZER(Program const& _input) +{ + // Solidity creates an invalid instruction for subobjects, so we simply + // ignore them in this fuzzer. + if (_input.has_obj()) + return; + bool filterStatefulInstructions = true; + bool filterUnboundedLoops = true; + bool filterMemoryWrites = true; + ProtoConverter converter( + filterStatefulInstructions, + filterUnboundedLoops, + filterMemoryWrites + ); + string yulSubObject = converter.programToString(_input); + // Fuzzer also fuzzes the EVM version field. + langutil::EVMVersion version = converter.version(); + EVMHost hostContext(version, evmone); + hostContext.reset(); + + // Do not proceed with tests that are too large. 1200 is an arbitrary + // threshold. + if (yulSubObject.size() > 1200) + return; + + YulStringRepository::reset(); + + // Package test case into a sub-object + Whiskers yulObjectFormat(R"( + object "main" { + code { + codecopy(0, dataoffset("deployed"), datasize("deployed")) + return(0, datasize("deployed")) + } + object "deployed" { + code { + + } + } + } + )"); + string yul_source = yulObjectFormat("fuzzerInput", yulSubObject).render(); + + if (const char* dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) + { + ofstream of(dump_path); + of.write(yul_source.data(), static_cast(yul_source.size())); + } + + solidity::frontend::OptimiserSettings settings = solidity::frontend::OptimiserSettings::none(); + AssemblyStack stackUnoptimized; + solAssert( + stackUnoptimized.parseAndAnalyze("source", yulSubObject), + "Parsing fuzzer generated input failed." + ); + ostringstream unoptimizedState; + yulFuzzerUtil::TerminationReason termReason = yulFuzzerUtil::interpret( + unoptimizedState, + stackUnoptimized.parserResult()->code, + EVMDialect::strictAssemblyForEVMObjects(version), + true + ); + if (yulFuzzerUtil::resourceLimitsExceeded(termReason)) + return; + + AssemblyStack stackOptimized; + solAssert( + stackOptimized.parseAndAnalyze("source", yulSubObject), + "Parsing fuzzer generated input failed." + ); + YulOptimizerTestCommon optimizerTest( + stackOptimized.parserResult(), + EVMDialect::strictAssemblyForEVMObjects(version) + ); + // HACK: Force this to fake stack limit evader for now + string step = "fakeStackLimitEvader"; + optimizerTest.setStep(step); +// optimizerTest.setStep(optimizerTest.randomOptimiserStep(_input.step())); + shared_ptr astBlock = optimizerTest.run(); + string optimisedProgram = Whiskers(R"( + object "main" { + code { + codecopy(0, dataoffset("deployed"), datasize("deployed")) + return(0, datasize("deployed")) + } + object "deployed" { + code { + + } + } + } + )") + ("fuzzerInput", AsmPrinter{}(*astBlock)) + .render(); + bytes optimisedByteCode; + optimisedByteCode = YulAssembler{version, settings, optimisedProgram}.assemble(); + + // Reset host before running optimised code. + hostContext.reset(); + evmc::result deployResultOpt = YulEvmoneUtility{}.deployCode(optimisedByteCode, hostContext); + solAssert( + deployResultOpt.status_code == EVMC_SUCCESS, + "Evmone: Optimized contract creation failed" + ); + auto callMessageOpt = YulEvmoneUtility{}.callMessage(deployResultOpt.create_address); + evmc::result callResultOpt = hostContext.call(callMessageOpt); + bool noRevertInSource = yulSubObject.find("revert") == string::npos; + bool noInvalidInSource = yulSubObject.find("invalid") == string::npos; + if (noRevertInSource) + solAssert( + callResultOpt.status_code != EVMC_REVERT, + "SolidityEvmoneInterface: EVM One reverted" + ); + if (noInvalidInSource) + solAssert( + callResultOpt.status_code != EVMC_INVALID_INSTRUCTION, + "Invalid instruction." + ); + solAssert( + (callResultOpt.status_code == EVMC_SUCCESS || + (!noRevertInSource && callResultOpt.status_code == EVMC_REVERT) || + (!noInvalidInSource && callResultOpt.status_code == EVMC_INVALID_INSTRUCTION)), + "Optimised call failed." + ); + ostringstream optimizedState; + optimizedState << EVMHostPrinter{hostContext, deployResultOpt.create_address}.storageOnly(); + + if (unoptimizedState.str() != optimizedState.str()) + { + cout << unoptimizedState.str() << endl; + cout << optimizedState.str() << endl; + } + solAssert( + unoptimizedState.str() == optimizedState.str(), + "State of unoptimised and optimised stack reused code do not match." + ); +} diff --git a/test/tools/ossfuzz/protoToYul.cpp b/test/tools/ossfuzz/protoToYul.cpp index c58a56ad2..b525d9491 100644 --- a/test/tools/ossfuzz/protoToYul.cpp +++ b/test/tools/ossfuzz/protoToYul.cpp @@ -1317,7 +1317,8 @@ void ProtoConverter::visit(Statement const& _x) visit(_x.ifstmt()); break; case Statement::kStorageFunc: - visit(_x.storage_func()); + if (!m_filterMemoryWrites || (_x.storage_func().has_st() && _x.storage_func().st() == StoreFunc_Storage_SSTORE)) + visit(_x.storage_func()); break; case Statement::kBlockstmt: if (_x.blockstmt().statements_size() > 0) @@ -1869,6 +1870,8 @@ void ProtoConverter::visit(Program const& _x) { case Program::kBlock: m_output << "{\n"; + // TODO: Clean up + m_output << "mstore(0x40, memoryguard(0x60))\n"; visit(_x.block()); m_output << "}\n"; break; diff --git a/test/tools/ossfuzz/protoToYul.h b/test/tools/ossfuzz/protoToYul.h index 9100b490c..3d4cc41b1 100644 --- a/test/tools/ossfuzz/protoToYul.h +++ b/test/tools/ossfuzz/protoToYul.h @@ -41,7 +41,8 @@ class ProtoConverter public: ProtoConverter( bool _filterStatefulInstructions = false, - bool _filterUnboundedLoops = false + bool _filterUnboundedLoops = false, + bool _filterMemoryWrites = false ) { m_funcVars = std::vector>>{}; @@ -59,6 +60,7 @@ public: m_forInitScopeExtEnabled = true; m_filterStatefulInstructions = _filterStatefulInstructions; m_filterUnboundedLoops = _filterUnboundedLoops; + m_filterMemoryWrites = _filterMemoryWrites; } ProtoConverter(ProtoConverter const&) = delete; ProtoConverter(ProtoConverter&&) = delete; @@ -381,5 +383,8 @@ private: /// Flat that, if set, stops the converter from generating potentially /// unbounded loops. bool m_filterUnboundedLoops; + /// Flag that, if set, stops the converter from generating memory + /// writes i.e., mstore/mstore8. + bool m_filterMemoryWrites; }; } diff --git a/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp b/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp index e86210ca3..4a097d2b7 100644 --- a/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp +++ b/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp @@ -93,8 +93,7 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size) termReason = yulFuzzerUtil::interpret( os2, stack.parserResult()->code, - EVMDialect::strictAssemblyForEVMObjects(langutil::EVMVersion()), - (yul::test::yul_fuzzer::yulFuzzerUtil::maxSteps * 4) + EVMDialect::strictAssemblyForEVMObjects(langutil::EVMVersion()) ); if (yulFuzzerUtil::resourceLimitsExceeded(termReason)) diff --git a/test/tools/ossfuzz/yulFuzzerCommon.cpp b/test/tools/ossfuzz/yulFuzzerCommon.cpp index 68c6ec5d6..890aad3b8 100644 --- a/test/tools/ossfuzz/yulFuzzerCommon.cpp +++ b/test/tools/ossfuzz/yulFuzzerCommon.cpp @@ -26,6 +26,7 @@ yulFuzzerUtil::TerminationReason yulFuzzerUtil::interpret( ostream& _os, shared_ptr _ast, Dialect const& _dialect, + bool _outputStorageOnly, size_t _maxSteps, size_t _maxTraceSize, size_t _maxExprNesting @@ -70,7 +71,10 @@ yulFuzzerUtil::TerminationReason yulFuzzerUtil::interpret( reason = TerminationReason::ExplicitlyTerminated; } - state.dumpTraceAndState(_os); + if (_outputStorageOnly) + state.dumpStorage(_os); + else + state.dumpTraceAndState(_os); return reason; } diff --git a/test/tools/ossfuzz/yulFuzzerCommon.h b/test/tools/ossfuzz/yulFuzzerCommon.h index 462811c1c..71411a708 100644 --- a/test/tools/ossfuzz/yulFuzzerCommon.h +++ b/test/tools/ossfuzz/yulFuzzerCommon.h @@ -36,6 +36,7 @@ struct yulFuzzerUtil std::ostream& _os, std::shared_ptr _ast, Dialect const& _dialect, + bool _outputStorageOnly = false, size_t _maxSteps = maxSteps, size_t _maxTraceSize = maxTraceSize, size_t _maxExprNesting = maxExprNesting diff --git a/test/tools/yulInterpreter/Interpreter.cpp b/test/tools/yulInterpreter/Interpreter.cpp index 5cd3c80c9..08c6aafd6 100644 --- a/test/tools/yulInterpreter/Interpreter.cpp +++ b/test/tools/yulInterpreter/Interpreter.cpp @@ -48,6 +48,13 @@ using namespace solidity::yul::test; using solidity::util::h256; +void InterpreterState::dumpStorage(ostream& _out) const +{ + for (auto const& slot: storage) + if (slot.second != h256{}) + _out << " " << slot.first.hex() << ": " << slot.second.hex() << endl; +} + void InterpreterState::dumpTraceAndState(ostream& _out) const { _out << "Trace:" << endl; @@ -61,9 +68,7 @@ void InterpreterState::dumpTraceAndState(ostream& _out) const if (value != 0) _out << " " << std::uppercase << std::hex << std::setw(4) << offset << ": " << h256(value).hex() << endl; _out << "Storage dump:" << endl; - for (auto const& slot: storage) - if (slot.second != h256{}) - _out << " " << slot.first.hex() << ": " << slot.second.hex() << endl; + dumpStorage(_out); } void Interpreter::run(InterpreterState& _state, Dialect const& _dialect, Block const& _ast) diff --git a/test/tools/yulInterpreter/Interpreter.h b/test/tools/yulInterpreter/Interpreter.h index 7af799bbd..b4674165c 100644 --- a/test/tools/yulInterpreter/Interpreter.h +++ b/test/tools/yulInterpreter/Interpreter.h @@ -99,7 +99,10 @@ struct InterpreterState size_t maxExprNesting = 0; ControlFlowState controlFlowState = ControlFlowState::Default; + /// Prints execution trace and non-zero storage to @param _out. void dumpTraceAndState(std::ostream& _out) const; + /// Prints non-zero storage to @param _out. + void dumpStorage(std::ostream& _out) const; }; /**