Stack optimizer fuzzer: Detect stack-too-deep during optimization

Co-authored-by: r0qs <deepmarolaest@gmail.com>
This commit is contained in:
Bhargava Shastry 2023-05-15 16:54:06 +02:00
parent 34d2383f28
commit 87d0c84960
6 changed files with 106 additions and 43 deletions

View File

@ -97,6 +97,7 @@ YulOptimizerTestCommon::YulOptimizerTestCommon(
}},
{"blockFlattener", [&]() {
disambiguate();
FunctionGrouper::run(*m_context, *m_ast);
BlockFlattener::run(*m_context, *m_ast);
}},
{"constantOptimiser", [&]() {

View File

@ -27,6 +27,9 @@
#include <libyul/backends/evm/EVMCodeTransform.h>
#include <libyul/backends/evm/EVMDialect.h>
#include <libyul/optimiser/CallGraphGenerator.h>
#include <libyul/CompilabilityChecker.h>
#include <libevmasm/Instruction.h>
#include <liblangutil/EVMVersion.h>
@ -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."
);
}
}

View File

@ -38,6 +38,11 @@ bytes YulAssembler::assemble()
return m_stack.assemble(YulStack::Machine::EVM).bytecode->bytecode;
}
std::shared_ptr<yul::Object> YulAssembler::object()
{
return m_stack.parserResult();
}
evmc::Result YulEvmoneUtility::deployCode(bytes const& _input, EVMHost& _host)
{
// Zero initialize all message fields

View File

@ -47,6 +47,7 @@ public:
m_optimiseYul(_optSettings.runYulOptimiser)
{}
solidity::bytes assemble();
std::shared_ptr<yul::Object> object();
private:
solidity::yul::YulStack m_stack;
std::string m_yulProgram;

View File

@ -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;

View File

@ -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;