diff --git a/test/libyul/YulInterpreterTest.cpp b/test/libyul/YulInterpreterTest.cpp index 42e0e7c6f543..28c0afc8d953 100644 --- a/test/libyul/YulInterpreterTest.cpp +++ b/test/libyul/YulInterpreterTest.cpp @@ -51,6 +51,7 @@ YulInterpreterTest::YulInterpreterTest(std::string const& _filename): m_source = m_reader.source(); m_expectation = m_reader.simpleExpectations(); m_simulateExternalCallsToSelf = m_reader.boolSetting("simulateExternalCall", false); + m_maxCost = m_reader.sizetSetting("maxCost", 0); } TestCase::TestResult YulInterpreterTest::run(std::ostream& _stream, std::string const& _linePrefix, bool const _formatted) @@ -76,6 +77,7 @@ std::string YulInterpreterTest::interpret(std::shared_ptr const& _ state.maxTraceSize = 32; state.maxSteps = 512; state.maxExprNesting = 64; + state.maxCost = m_maxCost; try { Interpreter::run( diff --git a/test/libyul/YulInterpreterTest.h b/test/libyul/YulInterpreterTest.h index b4a21b69a277..53e6655ab2c6 100644 --- a/test/libyul/YulInterpreterTest.h +++ b/test/libyul/YulInterpreterTest.h @@ -44,6 +44,7 @@ class YulInterpreterTest: public solidity::frontend::test::EVMVersionRestrictedT std::string interpret(std::shared_ptr const& _object); bool m_simulateExternalCallsToSelf = false; + size_t m_maxCost = 0; }; } diff --git a/test/libyul/yulInterpreterTests/cost_limit_not_exceeded.yul b/test/libyul/yulInterpreterTests/cost_limit_not_exceeded.yul new file mode 100644 index 000000000000..bbaeb623129c --- /dev/null +++ b/test/libyul/yulInterpreterTests/cost_limit_not_exceeded.yul @@ -0,0 +1,13 @@ +{ + // Simple arithmetic with a generous maxCost should complete normally + // and not trigger InstructionLimitReached. + mstore(0, add(mul(3, 7), 1)) +} +// ==== +// maxCost: 10000 +// ---- +// Trace: +// Memory dump: +// 0: 0000000000000000000000000000000000000000000000000000000000000016 +// Storage dump: +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/instruction_limit_reached_exp.yul b/test/libyul/yulInterpreterTests/instruction_limit_reached_exp.yul new file mode 100644 index 000000000000..4749cb017ece --- /dev/null +++ b/test/libyul/yulInterpreterTests/instruction_limit_reached_exp.yul @@ -0,0 +1,14 @@ +{ + // exp(2, not(0)) charges 1 + 256 = 257 cost units (1 + bits in exponent). + // With maxCost: 100, this must trigger InstructionLimitReached. + let x := exp(2, not(0)) + mstore(0, x) +} +// ==== +// maxCost: 100 +// ---- +// Trace: +// Instruction cost limit reached. +// Memory dump: +// Storage dump: +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/instruction_limit_reached_keccak256.yul b/test/libyul/yulInterpreterTests/instruction_limit_reached_keccak256.yul new file mode 100644 index 000000000000..33aedbf3271f --- /dev/null +++ b/test/libyul/yulInterpreterTests/instruction_limit_reached_keccak256.yul @@ -0,0 +1,15 @@ +{ + // keccak256 charges 50 cost units per call (plus 1 for evalBuiltin base). + // Two calls = ~102 cost. With maxCost: 60, the second call triggers InstructionLimitReached. + let a := keccak256(0, 0) + let b := keccak256(0, 0) + mstore(0, xor(a, b)) +} +// ==== +// maxCost: 60 +// ---- +// Trace: +// Instruction cost limit reached. +// Memory dump: +// Storage dump: +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/large_memory_mload_straddles_zero.yul b/test/libyul/yulInterpreterTests/large_memory_mload_straddles_zero.yul new file mode 100644 index 000000000000..5d7235b5c42f --- /dev/null +++ b/test/libyul/yulInterpreterTests/large_memory_mload_straddles_zero.yul @@ -0,0 +1,18 @@ +{ + // Write distinct bytes on either side of the u256 wraparound boundary + mstore8(sub(0, 1), 0xaa) // byte at 0xfff...fff + mstore8(0, 0xbb) // byte at 0 + // mload at sub(0,1) reads 32 bytes wrapping around zero: + // byte 0: memory[0xfff...fff] = 0xaa + // byte 1: memory[0] = 0xbb + // bytes 2-31: 0 + sstore(0, mload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// 0: bb00000000000000000000000000000000000000000000000000000000000000 +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0: 00000000000000000000000000000000000000000000000000000000000000aa +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: aabb000000000000000000000000000000000000000000000000000000000000 +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/large_memory_mstore8_wrap.yul b/test/libyul/yulInterpreterTests/large_memory_mstore8_wrap.yul new file mode 100644 index 000000000000..e8251f3f24fc --- /dev/null +++ b/test/libyul/yulInterpreterTests/large_memory_mstore8_wrap.yul @@ -0,0 +1,14 @@ +{ + // mstore8 at max u256 address: single byte write, no range wrapping + mstore8(sub(0, 1), 0xab) + // mload at same address: reads 32 bytes wrapping around zero + // byte 0: memory[0xfff...fff] = 0xab, bytes 1-31: memory[0..30] = 0 + sstore(0, mload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0: 00000000000000000000000000000000000000000000000000000000000000ab +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: ab00000000000000000000000000000000000000000000000000000000000000 +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/large_memory_mstore_overwrites_at_wrap.yul b/test/libyul/yulInterpreterTests/large_memory_mstore_overwrites_at_wrap.yul new file mode 100644 index 000000000000..2c1a09194b61 --- /dev/null +++ b/test/libyul/yulInterpreterTests/large_memory_mstore_overwrites_at_wrap.yul @@ -0,0 +1,17 @@ +{ + // First mstore writes 0x11 at addresses 0..31 + mstore(0, 0x1111111111111111111111111111111111111111111111111111111111111111) + // Second mstore at sub(0,1) writes 0x22 at addresses 0xfff...fff, 0, 1, ..., 30 + // This overwrites bytes 0..30 with 0x22, leaving byte 31 as 0x11 + mstore(sub(0, 1), 0x2222222222222222222222222222222222222222222222222222222222222222) + // mload(0) reads bytes 0..31: 31 bytes of 0x22 followed by 0x11 + sstore(0, mload(0)) +} +// ---- +// Trace: +// Memory dump: +// 0: 2222222222222222222222222222222222222222222222222222222222222211 +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0: 0000000000000000000000000000000000000000000000000000000000000022 +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 2222222222222222222222222222222222222222222222222222222222222211 +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/large_memory_no_wrap.yul b/test/libyul/yulInterpreterTests/large_memory_no_wrap.yul new file mode 100644 index 000000000000..ef7d5b2ef0bf --- /dev/null +++ b/test/libyul/yulInterpreterTests/large_memory_no_wrap.yul @@ -0,0 +1,13 @@ +{ + // mstore at sub(0, 32) = 0xfff...ffe0: writes exactly 32 bytes ending at 0xfff...ffff, + // no address wrapping needed + mstore(sub(0, 32), 0xdeadbeef) + sstore(0, mload(sub(0, 32))) +} +// ---- +// Trace: +// Memory dump: +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0: 00000000000000000000000000000000000000000000000000000000deadbeef +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 00000000000000000000000000000000000000000000000000000000deadbeef +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/storage_adjacent_large_keys.yul b/test/libyul/yulInterpreterTests/storage_adjacent_large_keys.yul new file mode 100644 index 000000000000..fd2b5f97c773 --- /dev/null +++ b/test/libyul/yulInterpreterTests/storage_adjacent_large_keys.yul @@ -0,0 +1,21 @@ +{ + // Adjacent slots near max u256 are independent; no wraparound aliasing + sstore(sub(0, 1), 0x1111) + sstore(sub(0, 2), 0x2222) + sstore(sub(0, 3), 0x3333) + // Verify each slot holds its own value + sstore(0, sload(sub(0, 1))) + sstore(1, sload(sub(0, 2))) + sstore(2, sload(sub(0, 3))) +} +// ---- +// Trace: +// Memory dump: +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 0000000000000000000000000000000000000000000000000000000000001111 +// 0000000000000000000000000000000000000000000000000000000000000001: 0000000000000000000000000000000000000000000000000000000000002222 +// 0000000000000000000000000000000000000000000000000000000000000002: 0000000000000000000000000000000000000000000000000000000000003333 +// fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd: 0000000000000000000000000000000000000000000000000000000000003333 +// fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe: 0000000000000000000000000000000000000000000000000000000000002222 +// ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff: 0000000000000000000000000000000000000000000000000000000000001111 +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/storage_large_key.yul b/test/libyul/yulInterpreterTests/storage_large_key.yul new file mode 100644 index 000000000000..e6b3bfcb945a --- /dev/null +++ b/test/libyul/yulInterpreterTests/storage_large_key.yul @@ -0,0 +1,12 @@ +{ + // sstore/sload roundtrip at the maximum storage slot (sub(0,1) = 0xfff...fff) + sstore(sub(0, 1), 0xdeadbeef) + sstore(0, sload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 00000000000000000000000000000000000000000000000000000000deadbeef +// ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff: 00000000000000000000000000000000000000000000000000000000deadbeef +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/storage_overwrite_large_key.yul b/test/libyul/yulInterpreterTests/storage_overwrite_large_key.yul new file mode 100644 index 000000000000..39c68af3be20 --- /dev/null +++ b/test/libyul/yulInterpreterTests/storage_overwrite_large_key.yul @@ -0,0 +1,14 @@ +{ + // Overwriting a large slot: only the last written value persists + sstore(sub(0, 1), 0x1111) + sstore(sub(0, 1), 0x2222) + sstore(sub(0, 1), 0x3333) + sstore(0, sload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 0000000000000000000000000000000000000000000000000000000000003333 +// ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff: 0000000000000000000000000000000000000000000000000000000000003333 +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/storage_sload_uninitialized.yul b/test/libyul/yulInterpreterTests/storage_sload_uninitialized.yul new file mode 100644 index 000000000000..66791c0f1028 --- /dev/null +++ b/test/libyul/yulInterpreterTests/storage_sload_uninitialized.yul @@ -0,0 +1,10 @@ +{ + // sload of a slot that was never written returns 0 + sstore(0, sload(0xdeadbeef)) + sstore(1, sload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// Storage dump: +// Transient storage dump: diff --git a/test/libyul/yulInterpreterTests/storage_zero_and_max_independent.yul b/test/libyul/yulInterpreterTests/storage_zero_and_max_independent.yul new file mode 100644 index 000000000000..7b8f0f627329 --- /dev/null +++ b/test/libyul/yulInterpreterTests/storage_zero_and_max_independent.yul @@ -0,0 +1,18 @@ +{ + // Slot 0 and slot sub(0,1) are distinct; writing one does not affect the other + sstore(0, 0xaaaa) + sstore(sub(0, 1), 0xbbbb) + // Verify slot 0 is unchanged after writing to max slot + sstore(1, sload(0)) + // Verify max slot is unchanged after writing to slot 0 + sstore(2, sload(sub(0, 1))) +} +// ---- +// Trace: +// Memory dump: +// Storage dump: +// 0000000000000000000000000000000000000000000000000000000000000000: 000000000000000000000000000000000000000000000000000000000000aaaa +// 0000000000000000000000000000000000000000000000000000000000000001: 000000000000000000000000000000000000000000000000000000000000aaaa +// 0000000000000000000000000000000000000000000000000000000000000002: 000000000000000000000000000000000000000000000000000000000000bbbb +// ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff: 000000000000000000000000000000000000000000000000000000000000bbbb +// Transient storage dump: diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 31bdf34ee4f9..2c4220090cf5 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -7,6 +7,9 @@ target_link_libraries(yulrun PRIVATE yulInterpreter libsolc evmasm Boost::boost add_executable(solfuzzer afl_fuzzer.cpp fuzzer_common.cpp) target_link_libraries(solfuzzer PRIVATE libsolc evmasm Boost::boost Boost::program_options ${Boost_SYSTEM_LIBRARY}) +add_executable(const_opt_runner const_opt_runner.cpp fuzzer_common.cpp) +target_link_libraries(const_opt_runner PRIVATE libsolc evmasm Boost::boost Boost::program_options ${Boost_SYSTEM_LIBRARY}) + add_executable(yulopti yulopti.cpp) target_link_libraries(yulopti PRIVATE solidity Boost::boost Boost::program_options ${Boost_SYSTEM_LIBRARY}) diff --git a/test/tools/const_opt_runner.cpp b/test/tools/const_opt_runner.cpp new file mode 100644 index 000000000000..c32f85fc32eb --- /dev/null +++ b/test/tools/const_opt_runner.cpp @@ -0,0 +1,57 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-3.0 + +/** + * Standalone runner for the constant optimizer fuzzer target. + * Reads a binary input file and runs it through testConstantOptimizer, + * exactly as the ossfuzz target would, so performance can be analyzed + * without the libFuzzer infrastructure. + * + * Usage: const_opt_runner + */ + +#include + +#include +#include +#include + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::ifstream file(argv[1], std::ios::binary); + if (!file) + { + std::cerr << "Error: cannot open file '" << argv[1] << "'" << std::endl; + return 1; + } + + std::string input{ + std::istreambuf_iterator(file), + std::istreambuf_iterator() + }; + + FuzzerUtil::testConstantOptimizer(input, /*_quiet=*/false); + + return 0; +} diff --git a/test/tools/fuzzer_common.cpp b/test/tools/fuzzer_common.cpp index 8cc30c199507..0497f6fb7fdb 100644 --- a/test/tools/fuzzer_common.cpp +++ b/test/tools/fuzzer_common.cpp @@ -39,8 +39,6 @@ using namespace solidity::frontend; using namespace solidity::langutil; using namespace solidity::util; -static auto constexpr s_evmVersions = EVMVersion::allVersions(); - void FuzzerUtil::testCompilerJsonInterface(std::string const& _input, bool _optimize, bool _quiet) { if (!_quiet) @@ -77,13 +75,12 @@ void FuzzerUtil::forceSMT(StringMap& _input) void FuzzerUtil::testCompiler( StringMap& _input, bool _optimize, - unsigned _rand, + EVMVersion const& _evmVersion, bool _forceSMT, bool _compileViaYul ) { frontend::CompilerStack compiler; - EVMVersion evmVersion = s_evmVersions[_rand % s_evmVersions.size()]; frontend::OptimiserSettings optimiserSettings; if (_optimize) optimiserSettings = frontend::OptimiserSettings::standard(); @@ -109,9 +106,11 @@ void FuzzerUtil::testCompiler( }); } compiler.setSources(_input); - compiler.setEVMVersion(evmVersion); + compiler.setEVMVersion(_evmVersion); compiler.setOptimiserSettings(optimiserSettings); compiler.setViaIR(_compileViaYul); + // We need to set it to NoMetadata, or we would get UTF-8 issues, which are uninteresting + compiler.setMetadataFormat(frontend::CompilerStack::MetadataFormat::NoMetadata); try { compiler.compile(); diff --git a/test/tools/fuzzer_common.h b/test/tools/fuzzer_common.h index 6da501d92485..e536baa3f0c2 100644 --- a/test/tools/fuzzer_common.h +++ b/test/tools/fuzzer_common.h @@ -20,6 +20,7 @@ #include #include +#include /** * Functions to be used for fuzz-testing of various components. @@ -40,7 +41,7 @@ struct FuzzerUtil static void testCompiler( solidity::StringMap& _input, bool _optimize, - unsigned _rand, + solidity::langutil::EVMVersion const& _evmVersion, bool _forceSMT, bool _compileViaYul ); diff --git a/test/tools/ossfuzz/README.md b/test/tools/ossfuzz/README.md index d00cb3d5191f..878bf7b2bbc0 100644 --- a/test/tools/ossfuzz/README.md +++ b/test/tools/ossfuzz/README.md @@ -75,3 +75,34 @@ To be consistent and aid better evaluation of the utility of the fuzzing diction [1]: https://github.com/google/oss-fuzz [2]: https://github.com/google/oss-fuzz/issues/1114#issuecomment-360660201 + +## Executables generated + +- `yulProto_diff_ossfuzz.cpp`: exe is `yul_diff_ssa_cfg_ossfuzz`. Generates + random YUL via Protobuf, compiles with and without optimisation, compares via YUL + interpreter +- `yulProtoFuzzer.cpp`: exe is`yul_proto_ossfuzz`. Generates random YUL via Protobuf, + runs different optimizer steps, and hopes it crashes +- `strictasm_diff_ossfuzz.cpp`: exe is `strictasm_diff_ossfuzz`. Interprets random characters + as strict assembly code, compiles with and without optimisation, compares via + YUL interpreter +- `strictasm_opt_ossfuzz.cpp`: exe is `strictasm_opt_ossfuzz`. Interprets + random characters as strict assembly code, runs the optimizer, and hopes it + crashes +- `strictasm_assembly_ossfuzz.cpp`: exe is `strictasm_assembly_ossfuzz`. + Interprets random characters as strict assembly code, assembles it, and hopes + it crashes +- `const_opt_ossfuzz.cpp`: exe is `const_opt_ossfuzz`. Interprets random characters as some kind + of constants, runs the constant optimizer, and hopes it crashes +- `solProtoFuzzer.cpp` exe is `sol_proto_ossfuzz`. Generates random Solidity via Protobuf. + This is actually generating pre-determined code that returns known constants. + Runs via evmone (in-memory), asserts that it does not revert, test() function must return 0, + i.e. all constants must be returned correctly. +- `solc_ossfuzz.cpp`. exe is`solc_ossfuzz`. Interprets random characters as Solidity test case, + compiles and hopes it crashes. NOTE: does not work well it seems +- `solc_ossfuzz.cpp`: exe is `solc_mutator_ossfuzz`. Same as above, but with mutator included. + I sincerely think this mutator thing is junk. +- `StackReuseCodegenFuzzer.cpp`: exe is `stack_reuse_codegen_ossfuzz`. Generates random Yul via + Protobuf, compiles with and without stack-reuse optimisation (i.e. `optimizeStackAllocation`), + executes both via evmone, and asserts that the resulting EVM state is identical. The goal is to + catch miscompilations introduced by the stack-reuse code-generation pass. diff --git a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp index 5bd5dd359058..d1ef4fcd6f12 100644 --- a/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp +++ b/test/tools/ossfuzz/StackReuseCodegenFuzzer.cpp @@ -73,10 +73,10 @@ DEFINE_PROTO_FUZZER(Program const& _input) if (_input.has_obj()) return; bool filterStatefulInstructions = true; - bool filterUnboundedLoops = true; + bool filterOptimizationNoise = false; ProtoConverter converter( filterStatefulInstructions, - filterUnboundedLoops + filterOptimizationNoise ); std::string yul_source = converter.programToString(_input); // Do not fuzz the EVM Version field. diff --git a/test/tools/ossfuzz/protoToYul.cpp b/test/tools/ossfuzz/protoToYul.cpp index cc5f68e71ecf..d9aaed38a0eb 100644 --- a/test/tools/ossfuzz/protoToYul.cpp +++ b/test/tools/ossfuzz/protoToYul.cpp @@ -712,6 +712,19 @@ void ProtoConverter::visit(NullaryOp const& _x) m_output << dictionaryToken(); return; } + // The following instructions can e sued to easily distinguish optimized + // and unoptimized code, which will lead to a lot of false positives. + if ( + m_filterOptimizationNoise && + ( + op == NullaryOp::GAS || + op == NullaryOp::MSIZE + ) + ) + { + m_output << dictionaryToken(); + return; + } switch (op) { @@ -1460,7 +1473,7 @@ void ProtoConverter::visit(Statement const& _x) visit(_x.blockstmt()); break; case Statement::kForstmt: - if (_x.forstmt().for_body().statements_size() > 0 && !m_filterUnboundedLoops) + if (_x.forstmt().for_body().statements_size() > 0) visit(_x.forstmt()); break; case Statement::kBoundedforstmt: @@ -2008,6 +2021,12 @@ void ProtoConverter::buildObjectScopeTree(Object const& _x) m_objectScope.emplace(objectName, node); } +bytes ProtoConverter::createCalldata(CallData const& _x) +{ + std::string const& data = _x.raw_data(); + return bytes(data.begin(), data.end()); +} + void ProtoConverter::visit(Program const& _x) { // Initialize input size @@ -2016,6 +2035,9 @@ void ProtoConverter::visit(Program const& _x) // Record EVM Version m_evmVersion = evmVersionMapping(_x.ver()); + // Create Calldata + m_calldata = createCalldata(_x.calldata()); + // Program is either a Yul object or a block of // statements. switch (_x.program_oneof_case()) diff --git a/test/tools/ossfuzz/protoToYul.h b/test/tools/ossfuzz/protoToYul.h index 2ae54f68a0b1..d8e869b92890 100644 --- a/test/tools/ossfuzz/protoToYul.h +++ b/test/tools/ossfuzz/protoToYul.h @@ -41,7 +41,7 @@ class ProtoConverter public: ProtoConverter( bool _filterStatefulInstructions = false, - bool _filterUnboundedLoops = false + bool _filterOptimizationNoise = false ) { m_funcVars = std::vector>>{}; @@ -58,7 +58,7 @@ class ProtoConverter m_isObject = false; m_forInitScopeExtEnabled = true; m_filterStatefulInstructions = _filterStatefulInstructions; - m_filterUnboundedLoops = _filterUnboundedLoops; + m_filterOptimizationNoise = _filterOptimizationNoise; } ProtoConverter(ProtoConverter const&) = delete; ProtoConverter(ProtoConverter&&) = delete; @@ -69,6 +69,10 @@ class ProtoConverter { return m_evmVersion; } + const bytes& calldata() const + { + return m_calldata; + } private: void visit(BinaryOp const&); @@ -260,6 +264,9 @@ class ProtoConverter /// @param _x root object of the Yul protobuf specification. void buildObjectScopeTree(Object const& _x); + /// Creates calldata + std::vector createCalldata(CallData const& _x); + /// Returns a pseudo-random dictionary token. /// @param _p Enum that decides if the returned token is hex prefixed ("0x") or not /// @return Dictionary token at the index computed using a @@ -314,6 +321,7 @@ class ProtoConverter return m_objectId - 1; } + bytes m_calldata; std::ostringstream m_output; /// Variables in all function definitions std::vector>> m_funcVars; @@ -386,8 +394,9 @@ class ProtoConverter /// Flag that, if set, stops the converter from generating state changing /// opcodes. bool m_filterStatefulInstructions; - /// Flat that, if set, stops the converter from generating potentially - /// unbounded loops. - bool m_filterUnboundedLoops; + /// Flag that, if set, stops the converter from generating Yul code that + /// will generate values that can be used to easily distinguish generated + /// test cases from each other by the Yul optimizer. + bool m_filterOptimizationNoise; }; } diff --git a/test/tools/ossfuzz/solc_ossfuzz.cpp b/test/tools/ossfuzz/solc_ossfuzz.cpp index dc7e26a2aeae..75747cf64c74 100644 --- a/test/tools/ossfuzz/solc_ossfuzz.cpp +++ b/test/tools/ossfuzz/solc_ossfuzz.cpp @@ -19,41 +19,47 @@ #include #include +#include #include using namespace solidity::frontend::test; +static auto constexpr s_evmVersions = solidity::langutil::EVMVersion::allVersions(); + // Prototype as we can't use the FuzzerInterface.h header. extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size); extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size) { - if (_size <= 600) + // We don't limit the `_size`, because it can be limited by the fuzzing engine's configuration + // via `-max_len=N` + std::string input(reinterpret_cast(_data), _size); + if (input.find("experimental") != std::string::npos) + { + // We are not interested in testing `pragma experimental` as it is not stable and may be removed + return 0; + } + std::map sourceCode; + try + { + TestCaseReader t = TestCaseReader(std::istringstream(input)); + sourceCode = t.sources().sources; + const bool compileViaYul = _size % 3 == 1; + const bool optimize = _size % 2 == 0; + const solidity::langutil::EVMVersion evmVersion = s_evmVersions[_size % s_evmVersions.size()]; + const bool forceSMT = false; + FuzzerUtil::testCompiler( + sourceCode, + optimize, + evmVersion, + forceSMT, + compileViaYul + ); + } + catch (std::runtime_error const&) { - std::string input(reinterpret_cast(_data), _size); - std::map sourceCode; - try - { - TestCaseReader t = TestCaseReader(std::istringstream(input)); - sourceCode = t.sources().sources; - std::map settings = t.settings(); - bool compileViaYul = - settings.count("compileViaYul") && - (settings.at("compileViaYul") == "also" || settings.at("compileViaYul") == "true"); - bool optimize = settings.count("optimize") && settings.at("optimize") == "true"; - FuzzerUtil::testCompiler( - sourceCode, - optimize, - /*_rand=*/static_cast(_size), - /*forceSMT=*/true, - compileViaYul - ); - } - catch (std::runtime_error const&) - { - return 0; - } + return 0; } return 0; } diff --git a/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp b/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp index 9abf269c869a..73dd1113124c 100644 --- a/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp +++ b/test/tools/ossfuzz/strictasm_diff_ossfuzz.cpp @@ -80,6 +80,16 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size) { return 0; } + const bytes calldata = { + 0xe9, 0x96, 0x40, 0x7d, 0xa5, 0xda, 0xb0, 0x2d, + 0x97, 0xf5, 0xc3, 0x44, 0xd7, 0x65, 0x0a, 0xd8, + 0x2c, 0x14, 0x3a, 0xf3, 0xe7, 0x40, 0x0f, 0x1e, + 0x67, 0xce, 0x90, 0x44, 0x2e, 0x92, 0xdb, 0x88, + 0xb8, 0x43, 0x9c, 0x41, 0x42, 0x08, 0xf1, 0xd7, + 0x65, 0xe9, 0x7f, 0xeb, 0x7b, 0xb9, 0x56, 0x9f, + 0xc7, 0x60, 0x5f, 0x7c, 0xcd, 0xfb, 0x92, 0xcd, + 0x8e, 0xf3, 0x9b, 0xe4, 0x4f, 0x6c, 0x14, 0xde + }; std::ostringstream os1; std::ostringstream os2; @@ -90,6 +100,7 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size) // TODO: Add EOF support yulFuzzerUtil::TerminationReason termReason = yulFuzzerUtil::interpret( os1, + calldata, *stack.parserResult()->code(), /*disableMemoryTracing=*/true ); @@ -99,6 +110,7 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* _data, size_t _size) stack.optimize(); termReason = yulFuzzerUtil::interpret( os2, + calldata, *stack.parserResult()->code(), /*disableMemoryTracing=*/true ); diff --git a/test/tools/ossfuzz/yulFuzzerCommon.cpp b/test/tools/ossfuzz/yulFuzzerCommon.cpp index c34f4351c5d4..d2ddccb92dcc 100644 --- a/test/tools/ossfuzz/yulFuzzerCommon.cpp +++ b/test/tools/ossfuzz/yulFuzzerCommon.cpp @@ -23,30 +23,24 @@ using namespace solidity::yul::test::yul_fuzzer; yulFuzzerUtil::TerminationReason yulFuzzerUtil::interpret( std::ostream& _os, + bytes const& calldata, AST const& _ast, bool _disableMemoryTracing, bool _outputStorageOnly, size_t _maxSteps, size_t _maxTraceSize, - size_t _maxExprNesting + size_t _maxExprNesting, + size_t _maxCost ) { InterpreterState state; state.maxTraceSize = _maxTraceSize; state.maxSteps = _maxSteps; state.maxExprNesting = _maxExprNesting; + state.maxCost = _maxCost; // Add 64 bytes of pseudo-randomly generated calldata so that // calldata opcodes perform non trivial work. - state.calldata = { - 0xe9, 0x96, 0x40, 0x7d, 0xa5, 0xda, 0xb0, 0x2d, - 0x97, 0xf5, 0xc3, 0x44, 0xd7, 0x65, 0x0a, 0xd8, - 0x2c, 0x14, 0x3a, 0xf3, 0xe7, 0x40, 0x0f, 0x1e, - 0x67, 0xce, 0x90, 0x44, 0x2e, 0x92, 0xdb, 0x88, - 0xb8, 0x43, 0x9c, 0x41, 0x42, 0x08, 0xf1, 0xd7, - 0x65, 0xe9, 0x7f, 0xeb, 0x7b, 0xb9, 0x56, 0x9f, - 0xc7, 0x60, 0x5f, 0x7c, 0xcd, 0xfb, 0x92, 0xcd, - 0x8e, 0xf3, 0x9b, 0xe4, 0x4f, 0x6c, 0x14, 0xde - }; + state.calldata = calldata; TerminationReason reason = TerminationReason::None; try @@ -65,6 +59,10 @@ yulFuzzerUtil::TerminationReason yulFuzzerUtil::interpret( { reason = TerminationReason::ExpressionNestingLimitReached; } + catch (InstructionLimitReached const&) + { + reason = TerminationReason::InstructionLimitReached; + } catch (ExplicitlyTerminated const&) { reason = TerminationReason::ExplicitlyTerminated; @@ -82,5 +80,6 @@ bool yulFuzzerUtil::resourceLimitsExceeded(TerminationReason _reason) return _reason == yulFuzzerUtil::TerminationReason::StepLimitReached || _reason == yulFuzzerUtil::TerminationReason::TraceLimitReached || - _reason == yulFuzzerUtil::TerminationReason::ExpressionNestingLimitReached; + _reason == yulFuzzerUtil::TerminationReason::ExpressionNestingLimitReached || + _reason == yulFuzzerUtil::TerminationReason::InstructionLimitReached; } diff --git a/test/tools/ossfuzz/yulFuzzerCommon.h b/test/tools/ossfuzz/yulFuzzerCommon.h index dcfd0f833775..d2e039b1f2b5 100644 --- a/test/tools/ossfuzz/yulFuzzerCommon.h +++ b/test/tools/ossfuzz/yulFuzzerCommon.h @@ -29,6 +29,7 @@ struct yulFuzzerUtil StepLimitReached, TraceLimitReached, ExpressionNestingLimitReached, + InstructionLimitReached, None }; @@ -40,12 +41,14 @@ struct yulFuzzerUtil /// eliminator. static TerminationReason interpret( std::ostream& _os, + bytes const& calldata, AST const& _ast, bool _disableMemoryTracing = false, bool _outputStorageOnly = false, size_t _maxSteps = maxSteps, size_t _maxTraceSize = maxTraceSize, - size_t _maxExprNesting = maxExprNesting + size_t _maxExprNesting = maxExprNesting, + size_t _maxCost = maxCost ); /// @returns true if @param _reason for Yul interpreter terminating is @@ -55,6 +58,7 @@ struct yulFuzzerUtil static size_t constexpr maxSteps = 100; static size_t constexpr maxTraceSize = 75; static size_t constexpr maxExprNesting = 64; + static size_t constexpr maxCost = 10000; }; } diff --git a/test/tools/ossfuzz/yulProto.proto b/test/tools/ossfuzz/yulProto.proto index a22aacb0df03..98c96239016a 100644 --- a/test/tools/ossfuzz/yulProto.proto +++ b/test/tools/ossfuzz/yulProto.proto @@ -378,6 +378,10 @@ message Block { repeated Statement statements = 1; } +message CallData { + required bytes raw_data = 1; +} + message Object { required Code code = 1; optional Data data = 2; @@ -415,6 +419,7 @@ message Program { } required Version ver = 3; required uint32 step = 4; + required CallData calldata = 5; } package solidity.yul.test.yul_fuzzer; diff --git a/test/tools/ossfuzz/yulProto_diff_ossfuzz.cpp b/test/tools/ossfuzz/yulProto_diff_ossfuzz.cpp index 4edfbf7ba4ee..f9e2eac8fd2a 100644 --- a/test/tools/ossfuzz/yulProto_diff_ossfuzz.cpp +++ b/test/tools/ossfuzz/yulProto_diff_ossfuzz.cpp @@ -46,9 +46,10 @@ using namespace solidity::yul::test::yul_fuzzer; DEFINE_PROTO_FUZZER(Program const& _input) { - ProtoConverter converter; + ProtoConverter converter(false, true); std::string yul_source = converter.programToString(_input); EVMVersion version = converter.version(); + auto calldata = converter.calldata(); if (const char* dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) { @@ -81,42 +82,53 @@ DEFINE_PROTO_FUZZER(Program const& _input) yulAssert(false, "Proto fuzzer generated malformed program"); } + // Generate pseudo-random calldata using libfuzzer input std::ostringstream os1; std::ostringstream os2; // Disable memory tracing to avoid false positive reports // such as unused write to memory e.g., // { mstore(0, 1) } // that would be removed by the redundant store eliminator. - // TODO: Add EOF support yulFuzzerUtil::TerminationReason termReason = yulFuzzerUtil::interpret( os1, + calldata, *stack.parserResult()->code(), - /*disableMemoryTracing=*/true + /*disableMemoryTracing=*/true, + /*outputStorageOnly=*/false, + yulFuzzerUtil::maxSteps, + yulFuzzerUtil::maxTraceSize, + yulFuzzerUtil::maxExprNesting, + yulFuzzerUtil::maxCost ); if (yulFuzzerUtil::resourceLimitsExceeded(termReason)) return; - // TODO: Add EOF support - YulOptimizerTestCommon optimizerTest(stack.parserResult()); - optimizerTest.setStep(optimizerTest.randomOptimiserStep(_input.step())); - auto const* astRoot = optimizerTest.run(); - yulAssert(astRoot != nullptr, "Optimiser error."); - // TODO: Add EOF support - termReason = yulFuzzerUtil::interpret( - os2, - *optimizerTest.optimizedObject()->code(), - true - ); - if (yulFuzzerUtil::resourceLimitsExceeded(termReason)) + try { + YulOptimizerTestCommon optimizerTest(stack.parserResult()); + optimizerTest.setStep(optimizerTest.randomOptimiserStep(_input.step())); + auto const* astRoot = optimizerTest.run(); + yulAssert(astRoot != nullptr, "Optimiser error."); + termReason = yulFuzzerUtil::interpret( + os2, + calldata, + *optimizerTest.optimizedObject()->code(), + true + ); + if (yulFuzzerUtil::resourceLimitsExceeded(termReason)) + return; + + bool isTraceEq = (os1.str() == os2.str()); + if (!isTraceEq) + { + std::cout << os1.str() << std::endl; + std::cout << os2.str() << std::endl; + yulAssert(false, "Interpreted traces for optimized and unoptimized code differ."); + } return; - bool isTraceEq = (os1.str() == os2.str()); - if (!isTraceEq) - { - std::cout << os1.str() << std::endl; - std::cout << os2.str() << std::endl; - yulAssert(false, "Interpreted traces for optimized and unoptimized code differ."); + } catch (langutil::UnimplementedFeatureError const&) { + // Unimplemented feature, skip this input + return; } - return; } diff --git a/test/tools/yulInterpreter/EVMInstructionInterpreter.cpp b/test/tools/yulInterpreter/EVMInstructionInterpreter.cpp index 8081a9df6eec..5a73ace1d063 100644 --- a/test/tools/yulInterpreter/EVMInstructionInterpreter.cpp +++ b/test/tools/yulInterpreter/EVMInstructionInterpreter.cpp @@ -77,32 +77,54 @@ u256 readZeroExtended(bytes const& _data, u256 const& _offset) namespace solidity::yul::test { +template void copyZeroExtended( - std::map& _target, + T& _target, bytes const& _source, size_t _targetOffset, size_t _sourceOffset, size_t _size ) { - for (size_t i = 0; i < _size; ++i) - _target[_targetOffset + i] = (_sourceOffset + i < _source.size() ? _source[_sourceOffset + i] : 0); + if (_size == 0) + return; + // Erase the target range (positions become implicitly zero). + size_t const tgtUpper = _targetOffset + _size; + _target.erase(_target.lower_bound(_targetOffset), (tgtUpper >= _targetOffset) ? _target.lower_bound(tgtUpper) : _target.end()); + // Collect only non-zero bytes from the source range (keys are already in sorted order). + // Bytes past _source.size() are treated as zero and therefore skipped. + std::vector toInsert; + size_t const srcUpper = _sourceOffset + _size; + size_t const srcEnd = std::min(srcUpper >= _sourceOffset ? srcUpper : _source.size(), _source.size()); + for (size_t i = _sourceOffset; i < srcEnd; ++i) + if (_source[i] != 0) + toInsert.emplace_back(u256(_targetOffset) + (i - _sourceOffset), _source[i]); + _target.insert(toInsert.begin(), toInsert.end()); } +template void copyZeroExtendedWithOverlap( - std::map& _target, - std::map const& _source, + T& _target, + T2 const& _source, size_t _targetOffset, size_t _sourceOffset, size_t _size ) { - if (_targetOffset >= _sourceOffset) - for (size_t i = _size; i > 0; --i) - _target[_targetOffset + i - 1] = (_source.count(_sourceOffset + i - 1) != 0 ? _source.at(_sourceOffset + i - 1) : 0); - else - for (size_t i = 0; i < _size; ++i) - _target[_targetOffset + i] = (_source.count(_sourceOffset + i) != 0 ? _source.at(_sourceOffset + i) : 0); + if (_size == 0) + return; + // Collect only the entries that actually exist in the source range, already shifted + // to their target keys. We snapshot before erasing to handle the case where + // _target and _source alias the same map (e.g. MCOPY). + size_t const srcUpper = _sourceOffset + _size; + size_t const tgtUpper = _targetOffset + _size; + std::vector toInsert; + for (auto it = _source.lower_bound(_sourceOffset), end = (srcUpper >= _sourceOffset) ? _source.lower_bound(srcUpper) : _source.end(); it != end; ++it) + toInsert.emplace_back(u256(_targetOffset) + (it->first - u256(_sourceOffset)), it->second); + // Erase the target range (positions with no source entry become implicitly zero). + _target.erase(_target.lower_bound(_targetOffset), (tgtUpper >= _targetOffset) ? _target.lower_bound(tgtUpper) : _target.end()); + // Write the collected entries into the target. + _target.insert(toInsert.begin(), toInsert.end()); } } @@ -140,7 +162,17 @@ u256 EVMInstructionInterpreter::eval( case Instruction::SMOD: return arg[1] == 0 ? 0 : s2u(u2s(arg[0]) % u2s(arg[1])); case Instruction::EXP: + { + // Square-and-multiply costs O(bits in exponent). Charge one extra per bit. + if (arg[1] != 0) + { + size_t const bits = static_cast(msb(arg[1])) + 1; + chargeCost(1 + bits); + if (m_state.maxCost > 0 && m_state.cost >= m_state.maxCost) + BOOST_THROW_EXCEPTION(InstructionLimitReached()); + } return exp256(arg[0], arg[1]); + } case Instruction::NOT: return ~arg[0]; case Instruction::LT: @@ -204,6 +236,7 @@ u256 EVMInstructionInterpreter::eval( // --------------- blockchain stuff --------------- case Instruction::KECCAK256: { + chargeCost(50); if (!accessMemory(arg[0], arg[1])) return u256("0x1234cafe1234cafe1234cafe") + arg[0]; uint64_t offset = uint64_t(arg[0] & uint64_t(-1)); @@ -230,6 +263,7 @@ u256 EVMInstructionInterpreter::eval( case Instruction::CALLDATASIZE: return m_state.calldata.size(); case Instruction::CALLDATACOPY: + chargeCopyWordCost(arg[2]); if (accessMemory(arg[0], arg[2])) copyZeroExtended( m_state.memory, m_state.calldata, @@ -240,6 +274,7 @@ u256 EVMInstructionInterpreter::eval( case Instruction::CODESIZE: return m_state.code.size(); case Instruction::CODECOPY: + chargeCopyWordCost(arg[2]); if (accessMemory(arg[0], arg[2])) copyZeroExtended( m_state.memory, m_state.code, @@ -258,10 +293,13 @@ u256 EVMInstructionInterpreter::eval( case Instruction::BLOBBASEFEE: return m_state.blobbasefee; case Instruction::EXTCODESIZE: + chargeCost(50); return u256(keccak256(h256(arg[0]))) & 0xffffff; case Instruction::EXTCODEHASH: + chargeCost(50); return u256(keccak256(h256(arg[0] + 1))); case Instruction::EXTCODECOPY: + chargeCopyWordCost(arg[3]); if (accessMemory(arg[1], arg[3])) // TODO this way extcodecopy and codecopy do the same thing. copyZeroExtended( @@ -273,6 +311,7 @@ u256 EVMInstructionInterpreter::eval( case Instruction::RETURNDATASIZE: return m_state.returndata.size(); case Instruction::RETURNDATACOPY: + chargeCopyWordCost(arg[2]); if (accessMemory(arg[0], arg[2])) copyZeroExtended( m_state.memory, m_state.returndata, @@ -281,6 +320,7 @@ u256 EVMInstructionInterpreter::eval( logTrace(_instruction, arg); return 0; case Instruction::MCOPY: + chargeCopyWordCost(arg[2]); if (accessMemory(arg[1], arg[2]) && accessMemory(arg[0], arg[2])) copyZeroExtendedWithOverlap( m_state.memory, @@ -393,6 +433,7 @@ u256 EVMInstructionInterpreter::eval( ) ? 1 : 0; case Instruction::RETURN: { + chargeCopyWordCost(arg[1]); m_state.returndata = {}; if (accessMemory(arg[0], arg[1])) m_state.returndata = m_state.readMemory(arg[0], arg[1]); @@ -514,6 +555,7 @@ u256 EVMInstructionInterpreter::evalBuiltin( std::vector const& _evaluatedArguments ) { + chargeCost(1); if (_fun.instruction) return eval(*_fun.instruction, _evaluatedArguments); @@ -521,6 +563,7 @@ u256 EVMInstructionInterpreter::evalBuiltin( // Evaluate datasize/offset/copy instructions if (fun == "datasize" || fun == "dataoffset") { + chargeCost(50); std::string arg = formatLiteral(std::get(_arguments.at(0))); if (arg.length() < 32) arg.resize(32, 0); @@ -541,7 +584,8 @@ u256 EVMInstructionInterpreter::evalBuiltin( if ( _evaluatedArguments.at(2) != 0 && accessMemory(_evaluatedArguments.at(0), _evaluatedArguments.at(2)) - ) + ) { + chargeCost(_evaluatedArguments.at(2)); copyZeroExtended( m_state.memory, m_state.code, @@ -549,6 +593,7 @@ u256 EVMInstructionInterpreter::evalBuiltin( size_t(_evaluatedArguments.at(1) & std::numeric_limits::max()), size_t(_evaluatedArguments.at(2)) ); + } return 0; } @@ -557,6 +602,7 @@ u256 EVMInstructionInterpreter::evalBuiltin( if (fun == "linkersymbol") { + chargeCost(50); yulAssert(_arguments.size() == 1); yulAssert(std::holds_alternative(_arguments[0])); std::string const placeholder = formatLiteral(std::get(_arguments[0])); @@ -567,6 +613,7 @@ u256 EVMInstructionInterpreter::evalBuiltin( if (fun == "loadimmutable") { + chargeCost(50); yulAssert(_arguments.size() == 1); yulAssert(std::holds_alternative(_arguments[0])); std::string const identifier = formatLiteral(std::get(_arguments[0])); @@ -609,6 +656,7 @@ bool EVMInstructionInterpreter::accessMemory(u256 const& _offset, u256 const& _s bytes EVMInstructionInterpreter::readMemory(u256 const& _offset, u256 const& _size) { + chargeCost(_size); yulAssert(_size <= s_maxRangeSize, "Too large read."); bytes data(size_t(_size), uint8_t(0)); for (size_t i = 0; i < data.size(); ++i) @@ -735,6 +783,7 @@ std::pair EVMInstructionInterpreter::isInputMemoryPtrModified( h256 EVMInstructionInterpreter::blobHash(u256 const& _index) { + chargeCost(50); //< Blobhash is expensive yulAssert(m_evmVersion.hasBlobHash()); if (_index >= m_state.blobCommitments.size()) return util::FixedHash<32>{}; @@ -745,3 +794,42 @@ h256 EVMInstructionInterpreter::blobHash(u256 const& _index) yulAssert(hashedCommitment.size == 32); return hashedCommitment; } + +void EVMInstructionInterpreter::chargeCost(u256 const& _cost) +{ + if (m_state.maxCost == 0 || _cost == 0) + return; + + // More than size_t can handle + if (_cost > u256(std::numeric_limits::max())) + { + m_state.trace.emplace_back("Instruction cost limit reached."); + BOOST_THROW_EXCEPTION(InstructionLimitReached()); + } + + // wrap around check + if (m_state.cost + _cost <= m_state.cost) + { + m_state.trace.emplace_back("Instruction cost limit reached."); + BOOST_THROW_EXCEPTION(InstructionLimitReached()); + } + + m_state.cost += static_cast(_cost); + if (m_state.cost >= m_state.maxCost) + { + m_state.trace.emplace_back("Instruction cost limit reached."); + BOOST_THROW_EXCEPTION(InstructionLimitReached()); + } +} + +void EVMInstructionInterpreter::chargeCopyWordCost(u256 const& _size) +{ + if (m_state.maxCost == 0) + return; + + // Cap to s_maxRangeSize; anything larger won't actually copy (accessMemory rejects it). + size_t const cappedSize = size_t(std::min(_size, u256(s_maxRangeSize))); + size_t const words = (cappedSize + 31) / 32; + chargeCost(words); +} + diff --git a/test/tools/yulInterpreter/EVMInstructionInterpreter.h b/test/tools/yulInterpreter/EVMInstructionInterpreter.h index dac72ce6ae8d..b9f635a92a0f 100644 --- a/test/tools/yulInterpreter/EVMInstructionInterpreter.h +++ b/test/tools/yulInterpreter/EVMInstructionInterpreter.h @@ -48,8 +48,9 @@ namespace solidity::yul::test /// Copy @a _size bytes of @a _source at offset @a _sourceOffset to /// @a _target at offset @a _targetOffset. Behaves as if @a _source would /// continue with an infinite sequence of zero bytes beyond its end. +template void copyZeroExtended( - std::map& _target, + T& _target, bytes const& _source, size_t _targetOffset, size_t _sourceOffset, @@ -61,9 +62,10 @@ void copyZeroExtended( /// continue with an infinite sequence of zero bytes beyond its end. /// When target and source areas overlap, behaves as if the data was copied /// using an intermediate buffer. +template void copyZeroExtendedWithOverlap( - std::map& _target, - std::map const& _source, + T& _target, + T2 const& _source, size_t _targetOffset, size_t _sourceOffset, size_t _size @@ -131,6 +133,14 @@ class EVMInstructionInterpreter /// Does not adjust msize, use @a accessMemory for that void writeMemoryWord(u256 const& _offset, u256 const& _value); + /// Charges the cost of copying _size bytes + /// throws InstructionLimitReached if the new cost exceeds the maximum cost. + void chargeCopyWordCost(u256 const& _size); + + /// Charges the cost by the provided amount, and throws InstructionLimitReached + /// if the new cost exceeds the maximum cost. + void chargeCost(u256 const& _cost); + void logTrace( evmasm::Instruction _instruction, std::vector const& _arguments = {}, diff --git a/test/tools/yulInterpreter/Interpreter.h b/test/tools/yulInterpreter/Interpreter.h index 3dd6a4238d97..01d4a32e15fd 100644 --- a/test/tools/yulInterpreter/Interpreter.h +++ b/test/tools/yulInterpreter/Interpreter.h @@ -66,6 +66,10 @@ class ExpressionNestingLimitReached: public InterpreterTerminatedGeneric { }; +class InstructionLimitReached: public InterpreterTerminatedGeneric +{ +}; + enum class ControlFlowState { Default, @@ -110,6 +114,14 @@ struct InterpreterState size_t maxSteps = 0; size_t numSteps = 0; size_t maxExprNesting = 0; + + /// Estimated cost of instructions executed, used for fuzzing to avoid timeouts + /// Does not need to be very accurate. + size_t cost = 0; + /// Maximum cost of before termination by InstructionLimitReached + /// Zero means no limit. + size_t maxCost = 0; + ControlFlowState controlFlowState = ControlFlowState::Default; /// Number of the current state instance, used for recursion protection @@ -135,7 +147,7 @@ struct InterpreterState yulAssert(_size <= 0xffff, "Too large read."); bytes data(size_t(_size), uint8_t(0)); for (size_t i = 0; i < data.size(); ++i) - data[i] = memory[_offset + i]; + data[i] = memory[_offset + u256(i)]; return data; } };