mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Stack optimizer fuzzer: Detect stack-too-deep during optimization
Co-authored-by: r0qs <deepmarolaest@gmail.com>
This commit is contained in:
parent
34d2383f28
commit
87d0c84960
@ -97,6 +97,7 @@ YulOptimizerTestCommon::YulOptimizerTestCommon(
|
||||
}},
|
||||
{"blockFlattener", [&]() {
|
||||
disambiguate();
|
||||
FunctionGrouper::run(*m_context, *m_ast);
|
||||
BlockFlattener::run(*m_context, *m_ast);
|
||||
}},
|
||||
{"constantOptimiser", [&]() {
|
||||
|
@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user