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;
+}