From b674581d9b6eaf845eaa6ba50c2b981777525880 Mon Sep 17 00:00:00 2001 From: r0qs <457348+r0qs@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:01:31 +0200 Subject: [PATCH] Add fuzzer to ssa cfg pipeline --- .circleci/config.yml | 1 + scripts/ci/build_ossfuzz.sh | 2 +- .../Dockerfile.ubuntu.clang.ossfuzz | 21 +- test/tools/fuzzer_common.cpp | 4 +- test/tools/fuzzer_common.h | 3 +- test/tools/ossfuzz/CMakeLists.txt | 17 ++ .../ossfuzz/yulProto_diff_ssa_cfg_ossfuzz.cpp | 206 ++++++++++++++++++ 7 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 test/tools/ossfuzz/yulProto_diff_ssa_cfg_ossfuzz.cpp diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b596f2b172a..0918e2d1779f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -306,6 +306,7 @@ commands: - test/tools/ossfuzz/strictasm_diff_ossfuzz - test/tools/ossfuzz/strictasm_opt_ossfuzz - test/tools/ossfuzz/yul_proto_diff_ossfuzz + - test/tools/ossfuzz/yul_proto_diff_ssa_cfg_ossfuzz - test/tools/ossfuzz/yul_proto_diff_custom_mutate_ossfuzz - test/tools/ossfuzz/yul_proto_ossfuzz - test/tools/ossfuzz/sol_proto_ossfuzz diff --git a/scripts/ci/build_ossfuzz.sh b/scripts/ci/build_ossfuzz.sh index 64d992994ed6..5f4998917d3f 100755 --- a/scripts/ci/build_ossfuzz.sh +++ b/scripts/ci/build_ossfuzz.sh @@ -2,7 +2,7 @@ set -ex ROOTDIR="$(realpath "$(dirname "$0")/../..")" -BUILDDIR="${ROOTDIR}/build" +BUILDDIR="${ROOTDIR}/build_ossfuzz" mkdir -p "${BUILDDIR}" && mkdir -p "$BUILDDIR/deps" function generate_protobuf_bindings diff --git a/scripts/docker/buildpack-deps/Dockerfile.ubuntu.clang.ossfuzz b/scripts/docker/buildpack-deps/Dockerfile.ubuntu.clang.ossfuzz index 5a96e6889dd0..05c9e682101f 100644 --- a/scripts/docker/buildpack-deps/Dockerfile.ubuntu.clang.ossfuzz +++ b/scripts/docker/buildpack-deps/Dockerfile.ubuntu.clang.ossfuzz @@ -35,8 +35,8 @@ RUN apt-get update; \ git \ jq \ libbz2-dev \ - libc++-18-dev \ - libc++abi-18-dev \ + libc++-dev \ + libc++abi-dev \ liblzma-dev \ libtool \ lsof \ @@ -67,6 +67,8 @@ RUN apt-get update; \ FROM base AS libraries # Boost +# FIXME: OSSFuzz requires -nostdinc++ which needs explicit libc++ include paths. +# See boost workaround: https://github.com/google/oss-fuzz/blob/master/projects/boost/build.sh#L19 and https://github.com/llvm/llvm-project/issues/57104#issuecomment-1649525043 RUN set -ex; \ cd /usr/src; \ wget -q 'https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2' -O boost.tar.bz2; \ @@ -78,12 +80,12 @@ RUN set -ex; \ export LDFLAGS="$LDFLAGS -stdlib=libc++ -lpthread"; \ ./bootstrap.sh --with-toolset=clang --prefix=/usr; \ ./b2 toolset=clang \ - cxxflags="${CXXFLAGS}" \ - linkflags="${LDFLAGS}" \ + cxxflags="$CXXFLAGS" \ + linkflags="$LDFLAGS" \ headers; \ ./b2 toolset=clang \ - cxxflags="${CXXFLAGS}" \ - linkflags="${LDFLAGS}" \ + cxxflags="$CXXFLAGS" \ + linkflags="$LDFLAGS" \ link=static variant=release runtime-link=static \ system filesystem unit_test_framework program_options \ install -j $(($(nproc)/2)); \ @@ -184,6 +186,13 @@ RUN set -ex; \ cp abicoder.hpp /usr/include; \ rm -rf /usr/src/Yul-Isabelle +# HEVM +RUN set -ex; \ + hevm_version="0.56.0"; \ + wget "https://github.com/ethereum/hevm/releases/download/release/${hevm_version}/hevm-x86_64-linux" -O /usr/bin/hevm; \ + test "$(sha256sum /usr/bin/hevm)" = "aabc7570a987bb87f1f2628ea80e284ce251ce444f36940933a1d47151d5bf09 /usr/bin/hevm"; \ + chmod +x /usr/bin/hevm + FROM base COPY --from=libraries /usr/lib /usr/lib COPY --from=libraries /usr/bin /usr/bin diff --git a/test/tools/fuzzer_common.cpp b/test/tools/fuzzer_common.cpp index 8cc30c199507..c2647f5dce4e 100644 --- a/test/tools/fuzzer_common.cpp +++ b/test/tools/fuzzer_common.cpp @@ -79,7 +79,8 @@ void FuzzerUtil::testCompiler( bool _optimize, unsigned _rand, bool _forceSMT, - bool _compileViaYul + bool _compileViaYul, + bool _ssaCfgCodegen ) { frontend::CompilerStack compiler; @@ -112,6 +113,7 @@ void FuzzerUtil::testCompiler( compiler.setEVMVersion(evmVersion); compiler.setOptimiserSettings(optimiserSettings); compiler.setViaIR(_compileViaYul); + compiler.setSSACFGCodegen(_ssaCfgCodegen); try { compiler.compile(); diff --git a/test/tools/fuzzer_common.h b/test/tools/fuzzer_common.h index 6da501d92485..d9d11966c9e2 100644 --- a/test/tools/fuzzer_common.h +++ b/test/tools/fuzzer_common.h @@ -42,7 +42,8 @@ struct FuzzerUtil bool _optimize, unsigned _rand, bool _forceSMT, - bool _compileViaYul + bool _compileViaYul, + bool _ssaCfgCodegen = false ); /// Adds the experimental SMTChecker pragma to each source file in the /// source map. diff --git a/test/tools/ossfuzz/CMakeLists.txt b/test/tools/ossfuzz/CMakeLists.txt index da18968928ea..f5132da91c10 100644 --- a/test/tools/ossfuzz/CMakeLists.txt +++ b/test/tools/ossfuzz/CMakeLists.txt @@ -14,6 +14,7 @@ if (OSSFUZZ) sol_proto_ossfuzz yul_proto_ossfuzz yul_proto_diff_ossfuzz + yul_proto_diff_ssa_cfg_ossfuzz yul_proto_diff_custom_mutate_ossfuzz stack_reuse_codegen_ossfuzz ) @@ -85,6 +86,21 @@ if (OSSFUZZ) target_compile_options(yul_proto_ossfuzz PUBLIC ${COMPILE_OPTIONS} ${SILENCE_PROTOBUF_AUTOGENERATED_WARNINGS}) + add_executable( + yul_proto_diff_ssa_cfg_ossfuzz + yulProto_diff_ssa_cfg_ossfuzz.cpp + protoToYul.cpp + yulProto.pb.cc + ) + target_include_directories(yul_proto_diff_ssa_cfg_ossfuzz PRIVATE /usr/include/libprotobuf-mutator) + target_link_libraries(yul_proto_diff_ssa_cfg_ossfuzz PRIVATE yul + protobuf-mutator-libfuzzer.a + protobuf-mutator.a + protobuf.a + ) + set_target_properties(yul_proto_diff_ssa_cfg_ossfuzz PROPERTIES LINK_FLAGS ${LIB_FUZZING_ENGINE}) + target_compile_options(yul_proto_diff_ssa_cfg_ossfuzz PUBLIC ${COMPILE_OPTIONS} ${SILENCE_PROTOBUF_AUTOGENERATED_WARNINGS}) + add_executable( yul_proto_diff_ossfuzz yulProto_diff_ossfuzz.cpp @@ -203,6 +219,7 @@ if (OSSFUZZ) ) set_target_properties(sol_proto_ossfuzz PROPERTIES LINK_FLAGS ${LIB_FUZZING_ENGINE}) target_compile_options(sol_proto_ossfuzz PUBLIC ${COMPILE_OPTIONS} ${SILENCE_PROTOBUF_AUTOGENERATED_WARNINGS}) + else() add_library(solc_ossfuzz solc_ossfuzz.cpp diff --git a/test/tools/ossfuzz/yulProto_diff_ssa_cfg_ossfuzz.cpp b/test/tools/ossfuzz/yulProto_diff_ssa_cfg_ossfuzz.cpp new file mode 100644 index 000000000000..5fd4a100904c --- /dev/null +++ b/test/tools/ossfuzz/yulProto_diff_ssa_cfg_ossfuzz.cpp @@ -0,0 +1,206 @@ +/* + 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 + +#include + +#include +#if (BOOST_VERSION < 108800) +#include +#else +#define BOOST_PROCESS_VERSION 1 +#include +#endif +#include + +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +using namespace solidity::util; +using namespace solidity::langutil; +using namespace solidity::yul; +using namespace solidity::yul::test; +using namespace solidity::yul::test::yul_fuzzer; + +bool checkEquivalenceHEVM( + std::string const& bytecode1, + std::string const& bytecode2, + std::string const& yulSource) +{ + namespace fs = boost::filesystem; + + auto writeTempFile = [](std::string const& bytecode, std::string const& prefix) -> std::string { + std::string filename = (fs::temp_directory_path() / fs::unique_path(prefix + "-%%%%%%%%.bin")).string(); + std::ofstream f(filename); + if (!f) + throw std::runtime_error("Failed to create temporary file: " + filename); + f << bytecode; + return filename; + }; + + std::string fileA = writeTempFile(bytecode1, "bytecode-a"); + std::string fileB = writeTempFile(bytecode2, "bytecode-b"); + + boost::process::ipstream outStream, errStream; + + std::vector args = { + "equivalence", + "--code-a-file", fileA, + "--code-b-file", fileB, + "--smttimeout", "1", + "--num-solvers", "1", + "--only-deployed" + }; + + auto hevmPath = boost::process::search_path("hevm"); + if (hevmPath.empty()) + throw std::runtime_error("HEVM not found in PATH."); + + boost::process::child hevmProcess( + hevmPath, + boost::process::args(args), + boost::process::std_out > outStream, + boost::process::std_err > errStream); + + std::ostringstream outBuffer, errBuffer; + auto readStream = [](boost::process::ipstream& stream, std::ostringstream& buffer) { + std::string line; + while (std::getline(stream, line)) + buffer << line << '\n'; + }; + + std::thread outThread(readStream, std::ref(outStream), std::ref(outBuffer)); + std::thread errThread(readStream, std::ref(errStream), std::ref(errBuffer)); + + hevmProcess.wait(); + outThread.join(); + errThread.join(); + + bool success = (hevmProcess.exit_code() == 0); + if (!success) + { + std::cout << "=== HEVM EQUIVALENCE CHECK FAILED ===" << std::endl; + std::cout << "Yul Source Input:\n" << yulSource << std::endl; + std::cout << "Bytecode length (Via-IR): " << bytecode1.length() << std::endl; + std::cout << "Bytecode length (SSA CFG): " << bytecode2.length() << std::endl; + std::cout << "HEVM output:\n" << outBuffer.str() << std::endl; + // FIXME: Hevm does not output to stderr in case of a mismatch, it outputs to stdout. + //std::cerr << "HEVM error:\n" << errBuffer.str() << std::endl; + std::cerr << "Bytecode files kept for analysis:\n Via-IR: " << fileA << "\n SSA CFG: " << fileB << std::endl; + } + else + { + fs::remove(fileA); + fs::remove(fileB); + } + + return success; +} + + +DEFINE_PROTO_FUZZER(Program const& _input) +{ + ProtoConverter converter; + std::string yul_source = converter.programToString(_input); + EVMVersion version = converter.version(); + + if (const char* dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) + { + std::ofstream of(dump_path); + of.write(yul_source.data(), static_cast(yul_source.size())); + } + + YulStringRepository::reset(); + + auto createParsedStack = [&]() -> YulStack { + YulStack stack( + version, + std::nullopt, + YulStack::Language::StrictAssembly, + solidity::frontend::OptimiserSettings::full(), + DebugInfoSelection::AllExceptExperimental() + ); + + if ( + !stack.parseAndAnalyze("source", yul_source) || + !stack.parserResult()->hasCode() || + !stack.parserResult()->analysisInfo || + Error::containsErrors(stack.errors()) + ) + { + SourceReferenceFormatter{std::cout, stack, false, false}.printErrorInformation(stack.errors()); + yulAssert(false, "Proto fuzzer generated malformed program"); + } + stack.optimize(); + return stack; + }; + + auto assemble = [&](bool _ssaCfgCodegen) -> std::pair { + YulStack stack = createParsedStack(); + MachineAssemblyObject evmAsm, runtimeAsm; + std::tie(evmAsm, runtimeAsm) = stack.assembleWithDeployed({}, _ssaCfgCodegen); + return std::make_pair(std::move(evmAsm), std::move(runtimeAsm)); + }; + + try + { + auto [evmAsm1, runtimeAsm1] = assemble(false); // Via-IR codegen + auto [evmAsm2, runtimeAsm2] = assemble(true); // SSA CFG codegen + + auto checkEquivalence = [&]( + std::string const& _kind, + auto const& _bytecode1, + auto const& _bytecode2 + ) { + if (_bytecode1 && _bytecode2) + { + std::string hex1 = _bytecode1->toHex(); + std::string hex2 = _bytecode2->toHex(); + + if (hex1 == hex2) + return; + + // If the bytecode differs, check equivalence using HEVM + if (!checkEquivalenceHEVM(hex1, hex2, yul_source)) + throw std::runtime_error(_kind + " bytecode differs:\n" + "Via IR: " + hex1 + "\n" + "SSA CFG: " + hex2); + } + }; + + checkEquivalence("Object", evmAsm1.bytecode, evmAsm2.bytecode); + checkEquivalence("Runtime Object", runtimeAsm1.bytecode, runtimeAsm2.bytecode); + } + catch (std::runtime_error const& e) + { + std::cout << "Error: " << e.what() << std::endl; + std::cout << "EVM Version: " << version.name() << std::endl; + yulAssert(false, "Bytecode differ between SSA CFG and IR codegen."); + } + + return; +}