From 87d0c849608d50ed9f6eb5bcfac9353d8d5e9eef Mon Sep 17 00:00:00 2001 From: Bhargava Shastry Date: Mon, 15 May 2023 16:54:06 +0200 Subject: [PATCH] Stack optimizer fuzzer: Detect stack-too-deep during optimization Co-authored-by: r0qs --- test/libyul/YulOptimizerTestCommon.cpp | 1 + .../tools/ossfuzz/StackReuseCodegenFuzzer.cpp | 116 ++++++++++++------ test/tools/ossfuzz/YulEvmoneInterface.cpp | 5 + test/tools/ossfuzz/YulEvmoneInterface.h | 1 + test/tools/ossfuzz/protoToYul.cpp | 17 ++- test/tools/ossfuzz/protoToYul.h | 9 +- 6 files changed, 106 insertions(+), 43 deletions(-) diff --git a/test/libyul/YulOptimizerTestCommon.cpp b/test/libyul/YulOptimizerTestCommon.cpp index 48c7ea403..92482814b 100644 --- a/test/libyul/YulOptimizerTestCommon.cpp +++ b/test/libyul/YulOptimizerTestCommon.cpp @@ -97,6 +97,7 @@ YulOptimizerTestCommon::YulOptimizerTestCommon( }}, {"blockFlattener", [&]() { disambiguate(); + FunctionGrouper::run(*m_context, *m_ast); BlockFlattener::run(*m_context, *m_ast); }}, {"constantOptimiser", [&]() { diff --git a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp index d7779a58d..fb502af52 100644 --- a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp +++ b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp @@ -27,6 +27,9 @@ #include #include +#include +#include + #include #include @@ -47,6 +50,24 @@ using namespace std; static evmc::VM evmone = evmc::VM{evmc_create_evmone()}; +namespace +{ +/// @returns true if there are recursive functions, false otherwise. +bool recursiveFunctionExists(Dialect const& _dialect, yul::Object& _object) +{ + auto recursiveFunctions = CallGraphGenerator::callGraph(*_object.code).recursiveFunctions(); + for(auto&& [function, variables]: CompilabilityChecker{ + _dialect, + _object, + true + }.unreachableVariables + ) + if(recursiveFunctions.count(function)) + return true; + return false; +} +} + DEFINE_PROTO_FUZZER(Program const& _input) { // Solidity creates an invalid instruction for subobjects, so we simply @@ -78,45 +99,58 @@ DEFINE_PROTO_FUZZER(Program const& _input) settings.runYulOptimiser = false; settings.optimizeStackAllocation = false; bytes unoptimisedByteCode; + bool recursiveFunction = false; + bool unoptimizedStackTooDeep = false; try { - unoptimisedByteCode = YulAssembler{version, nullopt, settings, yul_source}.assemble(); + YulAssembler assembler{version, nullopt, settings, yul_source}; + unoptimisedByteCode = assembler.assemble(); + auto yulObject = assembler.object(); + recursiveFunction = recursiveFunctionExists( + EVMDialect::strictAssemblyForEVMObjects(version), + *yulObject + ); } catch (solidity::yul::StackTooDeepError const&) { - return; + unoptimizedStackTooDeep = true; } - evmc::Result deployResult = YulEvmoneUtility{}.deployCode(unoptimisedByteCode, hostContext); - if (deployResult.status_code != EVMC_SUCCESS) - 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 - // we lazily check by string find, the EVM call should not revert. - bool noRevertInSource = yul_source.find("revert") == string::npos; - bool noInvalidInSource = yul_source.find("invalid") == string::npos; - if (noInvalidInSource) - solAssert( - callResult.status_code != EVMC_INVALID_INSTRUCTION, - "Invalid instruction." - ); - if (noRevertInSource) - solAssert( - callResult.status_code != EVMC_REVERT, - "SolidityEvmoneInterface: EVM One reverted" - ); - // Bail out on serious errors encountered during a call. - if (YulEvmoneUtility{}.seriousCallError(callResult.status_code)) - 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(); + bool noRevertInSource = true; + bool noInvalidInSource = true; + if (!unoptimizedStackTooDeep) + { + evmc::Result deployResult = YulEvmoneUtility{}.deployCode(unoptimisedByteCode, hostContext); + if (deployResult.status_code != EVMC_SUCCESS) + 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 + // we lazily check by string find, the EVM call should not revert. + noRevertInSource = yul_source.find("revert") == string::npos; + noInvalidInSource = yul_source.find("invalid") == string::npos; + if (noInvalidInSource) + solAssert( + callResult.status_code != EVMC_INVALID_INSTRUCTION, + "Invalid instruction." + ); + if (noRevertInSource) + solAssert( + callResult.status_code != EVMC_REVERT, + "SolidityEvmoneInterface: EVM One reverted" + ); + // Bail out on serious errors encountered during a call. + if (YulEvmoneUtility{}.seriousCallError(callResult.status_code)) + return; + solAssert( + (callResult.status_code == EVMC_SUCCESS || + (!noRevertInSource && callResult.status_code == EVMC_REVERT) || + (!noInvalidInSource && callResult.status_code == EVMC_INVALID_INSTRUCTION)), + "Unoptimised call failed." + ); + unoptimizedState << EVMHostPrinter{hostContext, deployResult.create_address}.state(); + } settings.runYulOptimiser = true; settings.optimizeStackAllocation = true; @@ -127,9 +161,14 @@ DEFINE_PROTO_FUZZER(Program const& _input) } catch (solidity::yul::StackTooDeepError const&) { - return; + if (!recursiveFunction) + throw; + else + return; } + if (unoptimizedStackTooDeep) + return; // Reset host before running optimised code. hostContext.reset(); evmc::Result deployResultOpt = YulEvmoneUtility{}.deployCode(optimisedByteCode, hostContext); @@ -158,8 +197,13 @@ DEFINE_PROTO_FUZZER(Program const& _input) ostringstream optimizedState; optimizedState << EVMHostPrinter{hostContext, deployResultOpt.create_address}.state(); - solAssert( - unoptimizedState.str() == optimizedState.str(), - "State of unoptimised and optimised stack reused code do not match." - ); + if (unoptimizedState.str() != optimizedState.str()) + { + cout << unoptimizedState.str() << endl; + cout << optimizedState.str() << endl; + solAssert( + false, + "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 f3a9f7445..b89245443 100644 --- a/test/tools/ossfuzz/YulEvmoneInterface.cpp +++ b/test/tools/ossfuzz/YulEvmoneInterface.cpp @@ -38,6 +38,11 @@ bytes YulAssembler::assemble() return m_stack.assemble(YulStack::Machine::EVM).bytecode->bytecode; } +std::shared_ptr YulAssembler::object() +{ + return m_stack.parserResult(); +} + evmc::Result YulEvmoneUtility::deployCode(bytes const& _input, EVMHost& _host) { // Zero initialize all message fields diff --git a/test/tools/ossfuzz/YulEvmoneInterface.h b/test/tools/ossfuzz/YulEvmoneInterface.h index 9224a949d..71a47507e 100644 --- a/test/tools/ossfuzz/YulEvmoneInterface.h +++ b/test/tools/ossfuzz/YulEvmoneInterface.h @@ -47,6 +47,7 @@ public: m_optimiseYul(_optSettings.runYulOptimiser) {} solidity::bytes assemble(); + std::shared_ptr object(); private: solidity::yul::YulStack m_stack; std::string m_yulProgram; diff --git a/test/tools/ossfuzz/protoToYul.cpp b/test/tools/ossfuzz/protoToYul.cpp index 25b5a977d..807685789 100644 --- a/test/tools/ossfuzz/protoToYul.cpp +++ b/test/tools/ossfuzz/protoToYul.cpp @@ -640,7 +640,7 @@ void ProtoConverter::visit(UnaryOp const& _x) { m_output << "mod("; visit(_x.operand()); - m_output << ", " << to_string(s_maxMemory) << ")"; + m_output << ", " << to_string(s_maxMemory - 32) << ")"; } else visit(_x.operand()); @@ -1125,12 +1125,21 @@ void ProtoConverter::visit(StoreFunc const& _x) // Write to memory within bounds, storage is unbounded if (storeType == StoreFunc::SSTORE) visit(_x.loc()); - else + else if (storeType == StoreFunc::MSTORE8) { m_output << "mod("; visit(_x.loc()); m_output << ", " << to_string(s_maxMemory) << ")"; } + else if (storeType == StoreFunc::MSTORE) + { + // Since we write 32 bytes, ensure it does not exceed + // upper bound on memory. + m_output << "mod("; + visit(_x.loc()); + m_output << ", " << to_string(s_maxMemory - 32) << ")"; + + } m_output << ", "; visit(_x.val()); m_output << ")\n"; @@ -1706,7 +1715,7 @@ void ProtoConverter::fillFunctionCallInput(unsigned _numInParams) case 1: { // Access memory within stipulated bounds - slot = "mod(" + dictionaryToken() + ", " + to_string(s_maxMemory) + ")"; + slot = "mod(" + dictionaryToken() + ", " + to_string(s_maxMemory - 32) + ")"; m_output << "mload(" << slot << ")"; break; } @@ -1940,6 +1949,8 @@ void ProtoConverter::visit(Program const& _x) { case Program::kBlock: m_output << "{\n"; + m_output << "mstore(memoryguard(0x10000), 1)\n"; + m_output << "sstore(mload(calldataload(0)), 1)\n"; visit(_x.block()); m_output << "}\n"; break; diff --git a/test/tools/ossfuzz/protoToYul.h b/test/tools/ossfuzz/protoToYul.h index 00559fa93..b7f7c72e5 100644 --- a/test/tools/ossfuzz/protoToYul.h +++ b/test/tools/ossfuzz/protoToYul.h @@ -344,11 +344,12 @@ private: static unsigned constexpr s_modOutputParams = 5; /// Hard-coded identifier for a Yul object's data block static auto constexpr s_dataIdentifier = "datablock"; - /// Upper bound on memory writes = 2**32 - 1 - /// See: https://eips.ethereum.org/EIPS/eip-1985#memory-size - static unsigned constexpr s_maxMemory = 4294967295; + /// Upper bound on memory writes is 64KB in order to + /// preserve semantic equivalence in the presence of + /// memory guard + static unsigned constexpr s_maxMemory = 65536; /// Upper bound on size for range copy functions - static unsigned constexpr s_maxSize = 65536; + static unsigned constexpr s_maxSize = 32768; /// Predicate to keep track of for body scope. If false, break/continue /// statements can not be created. bool m_inForBodyScope;