From 1598e6be6775a56ab42c7f27df4a59c00c7c118f Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Wed, 21 Aug 2024 13:01:35 -0700 Subject: [PATCH] EOF Differential Layout Fuzzer (#7488) Differential EOF Layout Fuzzer guided by Besu's layout parser. Signed-off-by: Danno Ferrin --- build.gradle | 4 + .../besu/evmtool/CodeValidateSubCommand.java | 10 +- .../evmtool/CodeValidationSubCommandTest.java | 20 +- .../evmtool/code-validate/createdata.json | 7 + .../besu/evmtool/code-validate/runtime.json | 2 +- .../besu/evmtool/trace/eof-section.json | 4 +- .../vm/GeneralStateReferenceTestTools.java | 4 +- .../besu/evm/tracing/StandardJsonTracer.java | 2 +- gradle/verification-metadata.xml | 21 ++ gradle/versions.gradle | 11 +- settings.gradle | 1 + testfuzz/README.md | 29 ++ testfuzz/build.gradle | 148 ++++++++++ .../hyperledger/besu/testfuzz/BesuFuzz.java | 38 +++ .../besu/testfuzz/BesuFuzzCommand.java | 78 ++++++ .../besu/testfuzz/EofContainerSubCommand.java | 258 ++++++++++++++++++ .../besu/testfuzz/ExternalClient.java | 22 ++ .../org/hyperledger/besu/testfuzz/Fuzzer.java | 239 ++++++++++++++++ .../besu/testfuzz/SingleQueryClient.java | 89 ++++++ .../besu/testfuzz/StreamingClient.java | 52 ++++ .../besu/testfuzz/VersionProvider.java | 47 ++++ testfuzz/src/main/scripts/unixStartScript.txt | 200 ++++++++++++++ .../src/main/scripts/windowsStartScript.txt | 91 ++++++ 23 files changed, 1351 insertions(+), 26 deletions(-) create mode 100644 ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/createdata.json create mode 100644 testfuzz/README.md create mode 100644 testfuzz/build.gradle create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzz.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzzCommand.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/ExternalClient.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/SingleQueryClient.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/StreamingClient.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/VersionProvider.java create mode 100644 testfuzz/src/main/scripts/unixStartScript.txt create mode 100644 testfuzz/src/main/scripts/windowsStartScript.txt diff --git a/build.gradle b/build.gradle index 7d2293b61e..6a4f279666 100644 --- a/build.gradle +++ b/build.gradle @@ -160,6 +160,10 @@ allprojects { url 'https://splunk.jfrog.io/splunk/ext-releases-local' content { includeGroupByRegex('com\\.splunk\\..*') } } + maven { + url 'https://gitlab.com/api/v4/projects/19871573/packages/maven' + content { includeGroupByRegex('com\\.gitlab\\.javafuzz(\\..*)?') } + } mavenCentral() diff --git a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/CodeValidateSubCommand.java b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/CodeValidateSubCommand.java index 4c04119864..a3fff8e5dc 100644 --- a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/CodeValidateSubCommand.java +++ b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/CodeValidateSubCommand.java @@ -36,8 +36,6 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; @@ -175,12 +173,8 @@ public class CodeValidateSubCommand implements Runnable { ((CodeV1) code).getEofLayout().containerMode().get())) { return "err: code is valid initcode. Runtime code expected"; } else { - return "OK " - + IntStream.range(0, code.getCodeSectionCount()) - .mapToObj(code::getCodeSection) - .map(cs -> code.getBytes().slice(cs.getEntryPoint(), cs.getLength())) - .map(Bytes::toUnprefixedHexString) - .collect(Collectors.joining(",")); + return "OK %d/%d/%d" + .formatted(code.getCodeSectionCount(), code.getSubcontainerCount(), code.getDataSize()); } } } diff --git a/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/CodeValidationSubCommandTest.java b/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/CodeValidationSubCommandTest.java index dd7133f862..d6b7359413 100644 --- a/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/CodeValidationSubCommandTest.java +++ b/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/CodeValidationSubCommandTest.java @@ -47,7 +47,7 @@ class CodeValidationSubCommandTest { EvmToolCommand parentCommand = new EvmToolCommand(bais, new PrintWriter(baos, true, UTF_8)); final CodeValidateSubCommand codeValidateSubCommand = new CodeValidateSubCommand(parentCommand); codeValidateSubCommand.run(); - assertThat(baos.toString(UTF_8)).contains("OK 00\n"); + assertThat(baos.toString(UTF_8)).contains("OK 1/0/0\n"); } @Test @@ -70,9 +70,9 @@ class CodeValidationSubCommandTest { assertThat(baos.toString(UTF_8)) .contains( """ - OK 00 + OK 1/0/0 err: layout - EOF header byte 1 incorrect - OK 5f5ff3 + OK 1/0/0 """); } @@ -85,7 +85,7 @@ class CodeValidationSubCommandTest { final CommandLine cmd = new CommandLine(codeValidateSubCommand); cmd.parseArgs(CODE_STOP_ONLY); codeValidateSubCommand.run(); - assertThat(baos.toString(UTF_8)).contains("OK 00\n"); + assertThat(baos.toString(UTF_8)).contains("OK 1/0/0\n"); } @Test @@ -112,9 +112,9 @@ class CodeValidationSubCommandTest { assertThat(baos.toString(UTF_8)) .contains( """ - OK 00 + OK 1/0/0 err: layout - EOF header byte 1 incorrect - OK 5f5ff3 + OK 1/0/0 """); } @@ -127,7 +127,7 @@ class CodeValidationSubCommandTest { final CommandLine cmd = new CommandLine(codeValidateSubCommand); cmd.parseArgs(CODE_RETURN_ONLY); codeValidateSubCommand.run(); - assertThat(baos.toString(UTF_8)).contains("OK 5f5ff3\n"); + assertThat(baos.toString(UTF_8)).contains("OK 1/0/0\n"); } @Test @@ -139,7 +139,7 @@ class CodeValidationSubCommandTest { final CommandLine cmd = new CommandLine(codeValidateSubCommand); cmd.parseArgs(CODE_INTERIOR_COMMENTS); codeValidateSubCommand.run(); - assertThat(baos.toString(UTF_8)).contains("OK 59595959e300015000,f8e4\n"); + assertThat(baos.toString(UTF_8)).contains("OK 2/0/0\n"); } @Test @@ -153,9 +153,9 @@ class CodeValidationSubCommandTest { assertThat(baos.toString(UTF_8)) .isEqualTo( """ - OK 00 + OK 1/0/0 err: layout - EOF header byte 1 incorrect - OK 5f5ff3 + OK 1/0/0 """); } } diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/createdata.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/createdata.json new file mode 100644 index 0000000000..9207cd65e0 --- /dev/null +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/createdata.json @@ -0,0 +1,7 @@ +{ + "cli": [ + "code-validate" + ], + "stdin": "0xef0001010004020001000b0300010014040004000080000436600060ff6000ec005000ef000101000402000100010400000000800000feda7ac0de", + "stdout": "OK 1/1/4\n" +} diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/runtime.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/runtime.json index 7965815559..fedeaa1bc0 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/runtime.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/code-validate/runtime.json @@ -3,5 +3,5 @@ "code-validate" ], "stdin": "ef00010100040200010001040000000080000000", - "stdout": "OK 00\n" + "stdout": "OK 1/0/0\n" } diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/eof-section.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/eof-section.json index 061d8f0940..439c69195a 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/eof-section.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/eof-section.json @@ -12,8 +12,8 @@ "stdin": "", "stdout": [ {"pc":0,"section":0,"op":227,"immediate":"0x0002","gas":"0x2540be400","gasCost":"0x5","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"CALLF"}, - {"pc":0,"section":2,"op":229,"immediate":"0x0002","gas":"0x2540be3fb","gasCost":"0x5","memSize":0,"stack":[],"depth":1,"fdepth":1,"refund":0,"opName":"JUMPF"}, - {"pc":0,"section":1,"op":228,"gas":"0x2540be3f6","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"fdepth":1,"refund":0,"opName":"RETF"}, + {"pc":0,"section":2,"op":229,"immediate":"0x0002","gas":"0x2540be3fb","gasCost":"0x5","memSize":0,"stack":[],"depth":1,"functionDepth":1,"refund":0,"opName":"JUMPF"}, + {"pc":0,"section":1,"op":228,"gas":"0x2540be3f6","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"functionDepth":1,"refund":0,"opName":"RETF"}, {"pc":3,"section":0,"op":97,"immediate":"0x2015","gas":"0x2540be3f3","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH2"}, {"pc":6,"section":0,"op":96,"immediate":"0x01","gas":"0x2540be3f0","gasCost":"0x3","memSize":0,"stack":["0x2015"],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":8,"section":0,"op":85,"gas":"0x2540be3ed","gasCost":"0x5654","memSize":0,"stack":["0x2015","0x1"],"depth":1,"refund":0,"opName":"SSTORE"}, diff --git a/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/vm/GeneralStateReferenceTestTools.java b/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/vm/GeneralStateReferenceTestTools.java index 42934b612d..354642f1ad 100644 --- a/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/vm/GeneralStateReferenceTestTools.java +++ b/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/vm/GeneralStateReferenceTestTools.java @@ -151,11 +151,11 @@ public class GeneralStateReferenceTestTools { .blobGasPricePerGas(blockHeader.getExcessBlobGas().orElse(BlobGas.ZERO)); final TransactionProcessingResult result = processor.processTransaction( - worldStateUpdater, + worldStateUpdater, blockHeader, transaction, blockHeader.getCoinbase(), - new CachingBlockHashLookup(blockHeader, blockchain), + new CachingBlockHashLookup(blockHeader, blockchain), false, TransactionValidationParams.processingBlock(), blobGasPrice); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/tracing/StandardJsonTracer.java b/evm/src/main/java/org/hyperledger/besu/evm/tracing/StandardJsonTracer.java index 2a71fb70cd..a289c665d2 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/tracing/StandardJsonTracer.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/tracing/StandardJsonTracer.java @@ -216,7 +216,7 @@ public class StandardJsonTracer implements OperationTracer { } sb.append("\"depth\":").append(depth).append(","); if (subdepth >= 1) { - sb.append("\"fdepth\":").append(subdepth).append(","); + sb.append("\"functionDepth\":").append(subdepth).append(","); } sb.append("\"refund\":").append(messageFrame.getGasRefund()).append(","); sb.append("\"opName\":\"").append(currentOp.getName()).append("\""); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 46029c87c8..49c1e7f00c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -546,6 +546,19 @@ + + + + + + + + + + + + + @@ -5372,6 +5385,14 @@ + + + + + + + + diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 57f2acae37..e0baf3bb46 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -41,6 +41,8 @@ dependencyManagement { dependency 'org.hyperledger.besu:besu-errorprone-checks:1.0.0' + dependency 'com.gitlab.javafuzz:core:1.26' + dependency 'com.google.guava:guava:33.0.0-jre' dependency 'com.graphql-java:graphql-java:21.5' @@ -153,8 +155,6 @@ dependencyManagement { } dependency 'org.fusesource.jansi:jansi:2.4.1' - dependency 'org.openjdk.jol:jol-core:0.17' - dependency 'tech.pegasys:jc-kzg-4844:1.0.0' dependencySet(group: 'org.hyperledger.besu', version: '0.9.4') { entry 'arithmetic' @@ -173,6 +173,9 @@ dependencyManagement { dependency 'org.java-websocket:Java-WebSocket:1.5.5' + dependency 'org.jacoco:org.jacoco.agent:0.8.11' + dependency 'org.jacoco:org.jacoco.core:0.8.11' + dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22' dependencySet(group: 'org.junit.jupiter', version: '5.10.1') { @@ -182,6 +185,8 @@ dependencyManagement { entry 'junit-jupiter-params' } + dependency 'org.openjdk.jol:jol-core:0.17' + dependency 'org.junit.platform:junit-platform-runner:1.9.2' dependency 'org.junit.vintage:junit-vintage-engine:5.10.1' @@ -232,6 +237,8 @@ dependencyManagement { dependency 'org.apache.maven:maven-artifact:3.9.6' + dependency 'tech.pegasys:jc-kzg-4844:1.0.0' + dependency 'tech.pegasys.discovery:discovery:22.12.0' } } diff --git a/settings.gradle b/settings.gradle index 09a8d20d4c..322383b32c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -68,5 +68,6 @@ include 'privacy-contracts' include 'services:kvstore' include 'services:pipeline' include 'services:tasks' +include 'testfuzz' include 'testutil' include 'util' diff --git a/testfuzz/README.md b/testfuzz/README.md new file mode 100644 index 0000000000..8f436d7011 --- /dev/null +++ b/testfuzz/README.md @@ -0,0 +1,29 @@ +# BesuFuzz + +BesuFuzz is where all the besu guided fuzzing tools live. + +## eof-container + +Performs differential fuzzing between Ethereum clients based on +the [txparse eofparse](https://github.com/holiman/txparse/blob/main/README.md#eof-parser-eofparse) +format. Note that only the inital `OK` and `err` values are used to determine if +there is a difference. + +### Prototypical CLI Usage: + +```shell +BesuFuzz eof-container \ + --tests-dir=~/git/ethereum/tests/EOFTests \ + --client=evm1=evmone-eofparse \ + --client=revm=revme bytecode +``` + +### Prototypical Gradle usage: + +```shell +./gradlew fuzzEvmone fuzzReth +``` + +There are pre-written Gradle targets for `fuzzEthereumJS`, `fuzzEvmone`, +`fuzzGeth`, `fuzzNethermind`, and `fuzzReth`. Besu is always a fuzzing target. +The `fuzzAll` target will fuzz all clients. \ No newline at end of file diff --git a/testfuzz/build.gradle b/testfuzz/build.gradle new file mode 100644 index 0000000000..1c96798155 --- /dev/null +++ b/testfuzz/build.gradle @@ -0,0 +1,148 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'application' +apply plugin: 'java-library' +apply plugin: 'jacoco' + +jar { + archiveBaseName = 'besu-test-fuzz' + manifest { + attributes( + 'Specification-Title': archiveBaseName, + 'Specification-Version': project.version, + 'Implementation-Title': archiveBaseName, + 'Implementation-Version': calculateVersion() + ) + } +} + +dependencies { + implementation project(':besu') + implementation project(':crypto:algorithms') + implementation project(':datatypes') + implementation project(':ethereum:referencetests') + implementation project(':evm') + implementation project(':util') + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.gitlab.javafuzz:core' + implementation 'info.picocli:picocli' + implementation 'io.tmio:tuweni-bytes' + implementation 'org.jacoco:org.jacoco.agent' + implementation 'org.jacoco:org.jacoco.core' +} + +application { + applicationName = 'BesuFuzz' + mainClass = 'org.hyperledger.besu.testfuzz.BesuFuzz' + applicationDefaultJvmArgs = [ + '-javaagent:$APP_HOME/lib/jacocoagent.jar' + ] +} + +def corpusDir = "${buildDir}/generated/corpus" + +tasks.register("runFuzzer", JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'org.hyperledger.besu.testfuzz.BesuFuzz' + + args = [ + "eof-container", + "--tests-dir=${projectDir}/../ethereum/referencetests/src/reference-test/external-resources/EOFTests", + "--corpus-dir=${corpusDir}" + ] + doFirst { + mkdir corpusDir + } +} + +tasks.register("fuzzEvmone") { + doLast { + runFuzzer.args += "--client=evm1=evmone-eofparse" + } + finalizedBy("runFuzzer") +} + +tasks.register("fuzzEthereumJS") { + doLast { + runFuzzer.args += "--client=etjs=tsx ../../../ethereumjs/ethereumjs-monorepo/packages/evm/scripts/eofContainerValidator.ts" + } + finalizedBy("runFuzzer") +} + +tasks.register("fuzzGeth") { + doLast { + runFuzzer.args += "--client=geth=eofdump eofparser" + } + finalizedBy("runFuzzer") +} + +tasks.register("fuzzNethermind") { + doLast { + runFuzzer.args += "--client=neth=netheofparse -x" + } + finalizedBy("runFuzzer") +} + +tasks.register("fuzzReth") { + doLast { + runFuzzer.args += "--client=revm=revme bytecode" + } + finalizedBy("runFuzzer") +} + +tasks.register("fuzzAll") { + dependsOn fuzzEvm1, fuzzEthJS, fuzzGeth, fuzzNethermind, fuzzReth +} + +jacoco { + applyTo run + applyTo runFuzzer +} + +// Copies jacoco into the lib directory +tasks.register("copyJacoco", Copy) { + // The jacocoagent.jar is embedded within the jar + from zipTree(configurations.jacocoAgent.singleFile).filter { it.name == 'jacocoagent.jar' }.singleFile + into layout.buildDirectory.dir("install/${application.applicationName}/lib") +} + +installDist.finalizedBy copyJacoco + +startScripts { + defaultJvmOpts = [ + "-Dsecp256k1.randomize=false" + ] + unixStartScriptGenerator.template = resources.text.fromFile("${projectDir}/src/main/scripts/unixStartScript.txt") + windowsStartScriptGenerator.template = resources.text.fromFile("${projectDir}/src/main/scripts/windowsStartScript.txt") + doLast { tweakStartScript(startScripts) } +} + +static def tweakStartScript(createScriptTask) { + def shortenWindowsClasspath = { line -> + line.replaceAll(/^set CLASSPATH=.*$/, "set CLASSPATH=%APP_HOME%/lib/*") + } + + createScriptTask.unixScript.text = createScriptTask.unixScript.text.replace('BESU_HOME', '\$APP_HOME') + createScriptTask.windowsScript.text = createScriptTask.windowsScript.text.replace('BESU_HOME', '%~dp0..') + + // Prevent the error originating from the 8191 chars limit on Windows + createScriptTask.windowsScript.text = + createScriptTask.windowsScript + .readLines() + .collect(shortenWindowsClasspath) + .join('\r\n') +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzz.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzz.java new file mode 100644 index 0000000000..6cde758804 --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzz.java @@ -0,0 +1,38 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import org.hyperledger.besu.util.LogConfigurator; + +/** The main entry point for the EVM (Ethereum Virtual Machine) tool. */ +public final class BesuFuzz { + + /** Default constructor for the EvmTool class. */ + public BesuFuzz() { + // this is here only for Javadoc linting + } + + /** + * The main entry point for the EVM (Ethereum Virtual Machine) tool. + * + * @param args The command line arguments. + */ + public static void main(final String... args) { + LogConfigurator.setLevel("", "DEBUG"); + final BesuFuzzCommand besuFuzzCommand = new BesuFuzzCommand(); + + besuFuzzCommand.execute(args); + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzzCommand.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzzCommand.java new file mode 100644 index 0000000000..22165b5ffe --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/BesuFuzzCommand.java @@ -0,0 +1,78 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.besu.util.LogConfigurator; + +import java.io.InputStream; +import java.io.PrintWriter; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +/** + * This is the root command for the `BesuFuzz` command line tool. It is a collection of fuzzers that + * are guided by Besu's implementations. + */ +@Command( + description = "Executes Besu based fuzz tests", + abbreviateSynopsis = true, + name = "evm", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + sortOptions = false, + header = "Usage:", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + optionListHeading = "%nOptions:%n", + footerHeading = "%n", + footer = "Hyperledger Besu is licensed under the Apache License 2.0", + subcommands = {EofContainerSubCommand.class}) +@SuppressWarnings("java:S106") +public class BesuFuzzCommand implements Runnable { + + PrintWriter out; + InputStream in; + + /** Default Constructor */ + BesuFuzzCommand() { + // this method is here only for JavaDoc linting + } + + void execute(final String... args) { + execute(System.in, new PrintWriter(System.out, true, UTF_8), args); + } + + void execute(final InputStream input, final PrintWriter output, final String[] args) { + final CommandLine commandLine = new CommandLine(this).setOut(output); + out = output; + in = input; + + // don't require exact case to match enum values + commandLine.setCaseInsensitiveEnumValuesAllowed(true); + + commandLine.setExecutionStrategy(new CommandLine.RunLast()); + commandLine.execute(args); + } + + @Override + public void run() { + LogConfigurator.setLevel("", "OFF"); + System.out.println("No default command, please select a subcommand"); + System.exit(1); + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java new file mode 100644 index 0000000000..efe84296cf --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java @@ -0,0 +1,258 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import static org.hyperledger.besu.testfuzz.EofContainerSubCommand.COMMAND_NAME; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.referencetests.EOFTestCaseSpec; +import org.hyperledger.besu.evm.Code; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.MainnetEVMs; +import org.hyperledger.besu.evm.code.CodeInvalid; +import org.hyperledger.besu.evm.code.CodeV1; +import org.hyperledger.besu.evm.code.EOFLayout; +import org.hyperledger.besu.evm.code.EOFLayout.EOFContainerMode; +import org.hyperledger.besu.evm.internal.EvmConfiguration; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParser.Feature; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.Separators; +import com.fasterxml.jackson.core.util.Separators.Spacing; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.gitlab.javafuzz.core.AbstractFuzzTarget; +import org.apache.tuweni.bytes.Bytes; +import picocli.CommandLine; +import picocli.CommandLine.Option; + +/** Fuzzes the parsing and validation of an EOF container. */ +@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose +@CommandLine.Command( + name = COMMAND_NAME, + description = "Fuzzes EOF container parsing and validation", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) +public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnable { + + static final String COMMAND_NAME = "eof-container"; + + @Option( + names = {"--corpus-dir"}, + paramLabel = "", + description = "Directory to store corpus files") + private final Path corpusDir = Path.of("corpus"); + + @Option( + names = {"--tests-dir"}, + paramLabel = "", + description = "Directory where EOF tests references file tree lives") + private final Path testsDir = null; + + @Option( + names = {"--client"}, + paramLabel = "=", + description = "Add a client for differential fuzzing") + private final Map clients = new LinkedHashMap<>(); + + @CommandLine.ParentCommand private final BesuFuzzCommand parentCommand; + + static final ObjectMapper eofTestMapper = createObjectMapper(); + static final JavaType javaType = + eofTestMapper + .getTypeFactory() + .constructParametricType(Map.class, String.class, EOFTestCaseSpec.class); + + List externalClients = new ArrayList<>(); + EVM evm = MainnetEVMs.pragueEOF(EvmConfiguration.DEFAULT); + long validContainers; + long totalContainers; + + /** + * Default constructor for the EofContainerSubCommand class. This constructor initializes the + * parentCommand to null. + */ + public EofContainerSubCommand() { + this(null); + } + + /** + * Constructs a new EofContainerSubCommand with the specified parent command. + * + * @param parentCommand The parent command for this subcommand. + */ + public EofContainerSubCommand(final BesuFuzzCommand parentCommand) { + this.parentCommand = parentCommand; + } + + private static ObjectMapper createObjectMapper() { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setDefaultPrettyPrinter( + (new DefaultPrettyPrinter()) + .withSeparators( + Separators.createDefaultInstance().withObjectFieldValueSpacing(Spacing.BOTH)) + .withObjectIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE.withIndent(" ")) + .withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE.withIndent(" "))); + objectMapper.disable(Feature.AUTO_CLOSE_SOURCE); + SimpleModule serializers = new SimpleModule("Serializers"); + serializers.addSerializer(Address.class, ToStringSerializer.instance); + serializers.addSerializer(Bytes.class, ToStringSerializer.instance); + objectMapper.registerModule(serializers); + + return objectMapper; + } + + @Override + public void run() { + // load test dir into corpus dir + if (testsDir != null) { + File f = testsDir.toFile(); + if (f.isDirectory()) { + try (var files = Files.walk(f.toPath(), Integer.MAX_VALUE)) { + files.forEach( + ff -> { + File file = ff.toFile(); + if (file.isFile()) { + extractFile(file, corpusDir.toFile()); + } + }); + } catch (IOException e) { + parentCommand.out.println("Exception walking " + f + ": " + e.getMessage()); + } + } + } + + clients.forEach((k, v) -> externalClients.add(new StreamingClient(k, v.split(" ")))); + System.out.println("Fuzzing client set: " + clients.keySet()); + + try { + new Fuzzer(this, corpusDir.toString(), this::fuzzStats).start(); + } catch (NoSuchAlgorithmException + | ClassNotFoundException + | InvocationTargetException + | IllegalAccessException + | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + private void extractFile(final File f, final File initialCorpus) { + final Map eofTests; + try { + eofTests = eofTestMapper.readValue(f, javaType); + } catch (IOException e) { + // presume parse failed because it's a corpus file + return; + } + for (var entry : eofTests.entrySet()) { + int index = 0; + for (var vector : entry.getValue().getVector().entrySet()) { + try (FileOutputStream fos = + new FileOutputStream( + new File( + initialCorpus, + f.toPath().getFileName() + "_" + (index++) + "_" + vector.getKey()))) { + Bytes codeBytes = Bytes.fromHexString(vector.getValue().code()); + evm.getCodeUncached(codeBytes); + fos.write(codeBytes.toArrayUnsafe()); + } catch (IOException e) { + parentCommand.out.println("Invalid file " + f + ": " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + } + } + + @Override + public void fuzz(final byte[] bytes) { + Bytes eofUnderTest = Bytes.wrap(bytes); + String eofUnderTestHexString = eofUnderTest.toHexString(); + Code code = evm.getCodeUncached(eofUnderTest); + Map results = new LinkedHashMap<>(); + boolean mismatch = false; + for (var client : externalClients) { + String value = client.differentialFuzz(eofUnderTestHexString); + results.put(client.getName(), value); + if (value == null || value.startsWith("fail: ")) { + mismatch = true; // if an external client fails, always report it as an error + } + } + boolean besuValid = false; + String besuReason; + if (!code.isValid()) { + besuReason = ((CodeInvalid) code).getInvalidReason(); + } else if (code.getEofVersion() != 1) { + EOFLayout layout = EOFLayout.parseEOF(eofUnderTest); + if (layout.isValid()) { + besuReason = "Besu Parsing Error"; + parentCommand.out.println(layout.version()); + parentCommand.out.println(layout.invalidReason()); + parentCommand.out.println(code.getEofVersion()); + parentCommand.out.println(code.getClass().getName()); + System.exit(1); + mismatch = true; + } else { + besuReason = layout.invalidReason(); + } + } else if (EOFContainerMode.INITCODE.equals( + ((CodeV1) code).getEofLayout().containerMode().get())) { + besuReason = "Code is initcode, not runtime"; + } else { + besuReason = "OK"; + besuValid = true; + } + for (var entry : results.entrySet()) { + mismatch = + mismatch + || besuValid != entry.getValue().toUpperCase(Locale.getDefault()).startsWith("OK"); + } + if (mismatch) { + parentCommand.out.println("besu: " + besuReason); + for (var entry : results.entrySet()) { + parentCommand.out.println(entry.getKey() + ": " + entry.getValue()); + } + parentCommand.out.println("code: " + eofUnderTest.toUnprefixedHexString()); + parentCommand.out.println("size: " + eofUnderTest.size()); + parentCommand.out.println(); + } else { + if (besuValid) { + validContainers++; + } + totalContainers++; + } + } + + String fuzzStats() { + return " / %5.2f%% valid %d/%d" + .formatted((100.0 * validContainers) / totalContainers, validContainers, totalContainers); + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/ExternalClient.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/ExternalClient.java new file mode 100644 index 0000000000..e5505239ab --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/ExternalClient.java @@ -0,0 +1,22 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +interface ExternalClient { + + String getName(); + + String differentialFuzz(String data); +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java new file mode 100644 index 0000000000..e4ebb68cc8 --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java @@ -0,0 +1,239 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.MessageDigestFactory; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import com.gitlab.javafuzz.core.AbstractFuzzTarget; +import com.gitlab.javafuzz.core.Corpus; +import org.apache.tuweni.bytes.Bytes; +import org.jacoco.core.data.ExecutionData; +import org.jacoco.core.data.ExecutionDataReader; +import org.jacoco.core.data.IExecutionDataVisitor; +import org.jacoco.core.data.ISessionInfoVisitor; +import org.jacoco.core.data.SessionInfo; + +/** Ported from javafuzz because JaCoCo APIs changed. */ +@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose +public class Fuzzer { + private final AbstractFuzzTarget target; + private final Corpus corpus; + private final Object agent; + private final Method getExecutionDataMethod; + private long executionsInSample; + private long lastSampleTime; + private long totalExecutions; + private long totalCoverage; + + Supplier fuzzStats; + + /** + * Create a new fuzzer + * + * @param target The target to fuzz + * @param dirs the list of corpus dirs and files, comma separated. + * @param fuzzStats additional fuzzing data from the client + * @throws ClassNotFoundException If Jacoco RT is not found (because jacocoagent.jar is not + * loaded) + * @throws NoSuchMethodException If the wrong version of Jacoco is loaded + * @throws InvocationTargetException If the wrong version of Jacoco is loaded + * @throws IllegalAccessException If the wrong version of Jacoco is loaded + * @throws NoSuchAlgorithmException If the SHA-256 crypto algo cannot be loaded. + */ + public Fuzzer( + final AbstractFuzzTarget target, final String dirs, final Supplier fuzzStats) + throws ClassNotFoundException, + NoSuchMethodException, + InvocationTargetException, + IllegalAccessException, + NoSuchAlgorithmException { + this.target = target; + this.corpus = new Corpus(dirs); + this.fuzzStats = fuzzStats; + Class c = Class.forName("org.jacoco.agent.rt.RT"); + Method getAgentMethod = c.getMethod("getAgent"); + this.agent = getAgentMethod.invoke(null); + this.getExecutionDataMethod = agent.getClass().getMethod("getExecutionData", boolean.class); + fileNameForBuffer(new byte[0]); + } + + void writeCrash(final byte[] buf) { + Bytes hash = Hash.sha256(Bytes.wrap(buf)); + String filepath = "crash-" + hash.toUnprefixedHexString(); + try (FileOutputStream fos = new FileOutputStream(filepath)) { + fos.write(buf); + System.out.printf("crash was written to %s%n", filepath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + void logStats(final String type) { + long rss = + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024; + long endTime = System.currentTimeMillis(); + long execs_per_second = -1; + if ((endTime - this.lastSampleTime) != 0) { + execs_per_second = (this.executionsInSample * 1000 / (endTime - this.lastSampleTime)); + } + this.lastSampleTime = endTime; + this.executionsInSample = 0; + + System.out.printf( + "#%d %s cov: %d corp: %d exec/s: %d rss: %d MB %s%n", + this.totalExecutions, + type, + this.totalCoverage, + this.corpus.getLength(), + execs_per_second, + rss, + fuzzStats.get()); + } + + /** + * Runs the fuzzer until the VM is shut down + * + * @throws InvocationTargetException if the wrong version of jacoco is loaded + * @throws IllegalAccessException if the wrong version of jacoco is loaded + * @throws NoSuchAlgorithmException if our favorite hash algo is not loaded + */ + @SuppressWarnings("java:S2189") // the endless loop is on purpose + public void start() + throws InvocationTargetException, IllegalAccessException, NoSuchAlgorithmException { + System.out.printf("#0 READ units: %d%n", this.corpus.getLength()); + this.totalCoverage = 0; + this.totalExecutions = 0; + this.executionsInSample = 0; + this.lastSampleTime = System.currentTimeMillis(); + + Map hitMap = new HashMap<>(); + + while (true) { + byte[] buf = this.corpus.generateInput(); + // The next version will run this in a different thread. + try { + this.target.fuzz(buf); + } catch (Exception e) { + e.printStackTrace(System.out); + this.writeCrash(buf); + System.exit(1); + break; + } + + this.totalExecutions++; + this.executionsInSample++; + + long newCoverage = getHitCount(hitMap); + if (newCoverage > this.totalCoverage) { + this.totalCoverage = newCoverage; + this.corpus.putBuffer(buf); + this.logStats("NEW"); + + // If you want hex strings of new hits, uncomment the following. + // String filename = fileNameForBuffer(buf); + // try (var pw = + // new PrintWriter( + // new BufferedWriter( + // new OutputStreamWriter(new FileOutputStream(filename), UTF_8)))) { + // pw.println(Bytes.wrap(buf).toHexString()); + // System.out.println(filename); + // } catch (IOException e) { + // e.printStackTrace(System.out); + // } + } else if ((System.currentTimeMillis() - this.lastSampleTime) > 30000) { + this.logStats("PULSE"); + } + } + } + + private static String fileNameForBuffer(final byte[] buf) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigestFactory.create(MessageDigestFactory.SHA256_ALG); + md.update(buf); + byte[] digest = md.digest(); + return String.format("./new-%064x.hex", new BigInteger(1, digest)); + } + + private long getHitCount(final Map hitMap) + throws IllegalAccessException, InvocationTargetException { + byte[] dumpData = (byte[]) this.getExecutionDataMethod.invoke(this.agent, false); + ExecutionDataReader edr = new ExecutionDataReader(new ByteArrayInputStream(dumpData)); + HitCounter hc = new HitCounter(hitMap); + edr.setExecutionDataVisitor(hc); + edr.setSessionInfoVisitor(hc); + try { + edr.read(); + } catch (IOException e) { + e.printStackTrace(); + this.writeCrash(dumpData); + } + + return hc.getHits(); + } + + static class HitCounter implements IExecutionDataVisitor, ISessionInfoVisitor { + long hits = 0; + Map hitMap; + + public HitCounter(final Map hitMap) { + this.hitMap = hitMap; + } + + @Override + public void visitClassExecution(final ExecutionData executionData) { + int hit = 0; + for (boolean b : executionData.getProbes()) { + if (executionData.getName().startsWith("org/hyperledger/besu/testfuzz/") + || executionData.getName().startsWith("org/bouncycastle/") + || executionData.getName().startsWith("com/gitlab/javafuzz/")) { + continue; + } + if (b) { + hit++; + } + } + String name = executionData.getName(); + if (hitMap.containsKey(name)) { + if (hitMap.get(name) < hit) { + hitMap.put(name, hit); + } + } else { + hitMap.put(name, hit); + } + hits += hit; + } + + public long getHits() { + return hits; + } + + @Override + public void visitSessionInfo(final SessionInfo sessionInfo) { + // nothing to do. Data parser requires a session listener. + } + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/SingleQueryClient.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/SingleQueryClient.java new file mode 100644 index 0000000000..802a6011aa --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/SingleQueryClient.java @@ -0,0 +1,89 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose +class SingleQueryClient implements ExternalClient { + final String name; + String[] command; + Pattern okRegexp; + String okRegexpStr; + int okGroup; + Pattern failRegexp; + int failGroup; + String failRegexpStr; + + public SingleQueryClient( + final String clientName, + final String okRegexp, + final int okGroup, + final String errorRegexp, + final int failGroup, + final String... command) { + this.name = clientName; + this.okRegexp = Pattern.compile(okRegexp); + this.okRegexpStr = okRegexp; + this.okGroup = okGroup; + this.failRegexp = Pattern.compile(errorRegexp); + this.failGroup = failGroup; + this.failRegexpStr = errorRegexp; + this.command = command; + } + + @Override + public String getName() { + return name; + } + + @Override + @SuppressWarnings("java:S2142") + public String differentialFuzz(final String data) { + if (!data.startsWith("0xef")) { + return "err: invalid_magic"; + } + try { + List localCommand = new ArrayList<>(command.length + 1); + localCommand.addAll(Arrays.asList(command)); + localCommand.add(data); + Process p = new ProcessBuilder().command(localCommand).redirectErrorStream(true).start(); + if (!p.waitFor(1, TimeUnit.SECONDS)) { + System.out.println("Process Hang for " + name); + return "fail: process took more than 1 sec " + p.pid(); + } + String s = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + Matcher m = okRegexp.matcher(s); + if (m.find()) { + return "OK " + m.group(okGroup); + } + m = failRegexp.matcher(s); + if (m.find()) { + return "err: " + m.group(failGroup); + } + return "fail: SingleClientQuery failed to get data"; + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + return "fail: " + e.getMessage(); + } + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/StreamingClient.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/StreamingClient.java new file mode 100644 index 0000000000..a59cd9326f --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/StreamingClient.java @@ -0,0 +1,52 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +class StreamingClient implements ExternalClient { + final String name; + final BufferedReader reader; + final PrintWriter writer; + + public StreamingClient(final String clientName, final String... command) { + try { + Process p = new ProcessBuilder().redirectErrorStream(true).command(command).start(); + this.name = clientName; + this.reader = new BufferedReader(p.inputReader(StandardCharsets.UTF_8)); + this.writer = new PrintWriter(p.getOutputStream(), true, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public String differentialFuzz(final String data) { + try { + writer.println(data); + return reader.readLine(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } +} diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/VersionProvider.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/VersionProvider.java new file mode 100644 index 0000000000..6a184dd90e --- /dev/null +++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/VersionProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.testfuzz; + +import org.hyperledger.besu.BesuInfo; + +import picocli.CommandLine; + +/** + * The VersionProvider class is responsible for providing the version of the Hyperledger Besu EVM + * tool. It implements the IVersionProvider interface from the picocli library. + * + *

The getVersion method returns a string array containing the version of the Hyperledger Besu + * EVM tool. + */ +public class VersionProvider implements CommandLine.IVersionProvider { + + /** + * Default constructor for the VersionProvider class. This constructor does not perform any + * operations. + */ + public VersionProvider() { + // this constructor is here only for javadoc linting + } + + /** + * This method returns the version of the Hyperledger Besu EVM tool. + * + * @return A string array containing the version of the Hyperledger Besu EVM tool. + */ + @Override + public String[] getVersion() { + return new String[] {"Hyperledger Besu evm " + BesuInfo.shortVersion()}; + } +} diff --git a/testfuzz/src/main/scripts/unixStartScript.txt b/testfuzz/src/main/scripts/unixStartScript.txt new file mode 100644 index 0000000000..59d8b83fbc --- /dev/null +++ b/testfuzz/src/main/scripts/unixStartScript.txt @@ -0,0 +1,200 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## ${applicationName} start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: \$0 may be a link +PRG="\$0" +# Need this for relative symlinks. +while [ -h "\$PRG" ] ; do + ls=`ls -ld "\$PRG"` + link=`expr "\$ls" : '.*-> \\(.*\\)\$'` + if expr "\$link" : '/.*' > /dev/null; then + PRG="\$link" + else + PRG=`dirname "\$PRG"`"/\$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"\$PRG\"`/${appHomeRelativePath}" >/dev/null +APP_HOME="`pwd -P`" +cd "\$SAVED" >/dev/null + +APP_NAME="${applicationName}" +APP_BASE_NAME=`basename "\$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script. +DEFAULT_JVM_OPTS=${defaultJvmOpts} + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "\$*" +} + +die () { + echo + echo "\$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$classpath +<% if ( mainClassName.startsWith('--module ') ) { %>MODULE_PATH=$modulePath<% } %> + +# Determine the Java command to use to start the JVM. +if [ -n "\$JAVA_HOME" ] ; then + if [ -x "\$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="\$JAVA_HOME/jre/sh/java" + else + JAVACMD="\$JAVA_HOME/bin/java" + fi + if [ ! -x "\$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: \$JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "\$cygwin" = "false" -a "\$darwin" = "false" -a "\$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ \$? -eq 0 ] ; then + if [ "\$MAX_FD" = "maximum" -o "\$MAX_FD" = "max" ] ; then + MAX_FD="\$MAX_FD_LIMIT" + fi + ulimit -n \$MAX_FD + if [ \$? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: \$MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: \$MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if \$darwin; then + GRADLE_OPTS="\$GRADLE_OPTS \\"-Xdock:name=\$APP_NAME\\" \\"-Xdock:icon=\$APP_HOME/media/gradle.icns\\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "\$cygwin" = "true" -o "\$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "\$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "\$CLASSPATH"` +<% if ( mainClassName.startsWith('--module ') ) { %> MODULE_PATH=`cygpath --path --mixed "\$MODULE_PATH"`<% } %> + JAVACMD=`cygpath --unix "\$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in \$ROOTDIRSRAW ; do + ROOTDIRS="\$ROOTDIRS\$SEP\$dir" + SEP="|" + done + OURCYGPATTERN="(^(\$ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "\$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="\$OURCYGPATTERN|(\$GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "\$@" ; do + CHECK=`echo "\$arg"|egrep -c "\$OURCYGPATTERN" -` + CHECK2=`echo "\$arg"|egrep -c "^-"` ### Determine if an option + + if [ \$CHECK -ne 0 ] && [ \$CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args\$i`=`cygpath --path --ignore --mixed "\$arg"` + else + eval `echo args\$i`="\"\$arg\"" + fi + i=`expr \$i + 1` + done + case \$i in + 0) set -- ;; + 1) set -- "\$args0" ;; + 2) set -- "\$args0" "\$args1" ;; + 3) set -- "\$args0" "\$args1" "\$args2" ;; + 4) set -- "\$args0" "\$args1" "\$args2" "\$args3" ;; + 5) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" ;; + 6) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" ;; + 7) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" ;; + 8) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" ;; + 9) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" "\$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\\\n "\$i" | sed "s/'/'\\\\\\\\''/g;1s/^/'/;\\\$s/\\\$/' \\\\\\\\/" ; done + echo " " +} +APP_ARGS=`save "\$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- -javaagent:\$APP_HOME/lib/jacocoagent.jar \$DEFAULT_JVM_OPTS \$JAVA_OPTS \$${optsEnvironmentVar} <% if ( appNameSystemProperty ) { %>"\"-D${appNameSystemProperty}=\$APP_BASE_NAME\"" <% } %>-classpath "\"\$CLASSPATH\"" <% if ( mainClassName.startsWith('--module ') ) { %>--module-path "\"\$MODULE_PATH\"" <% } %>${mainClassName} "\$APP_ARGS" + +unset BESU_USING_JEMALLOC +if [ "\$darwin" = "false" -a "\$msys" = "false" ]; then + # check if jemalloc is available + TEST_JEMALLOC=\$(LD_PRELOAD=libjemalloc.so sh -c true 2>&1) + + # if jemalloc is available the output is empty, otherwise the output has an error line + if [ -z "\$TEST_JEMALLOC" ]; then + export LD_PRELOAD=libjemalloc.so + export BESU_USING_JEMALLOC=true + else + # jemalloc not available, as fallback limit malloc to 2 arenas + export MALLOC_ARENA_MAX=2 + fi +fi + +exec "\$JAVACMD" "\$@" diff --git a/testfuzz/src/main/scripts/windowsStartScript.txt b/testfuzz/src/main/scripts/windowsStartScript.txt new file mode 100644 index 0000000000..2ce40892ef --- /dev/null +++ b/testfuzz/src/main/scripts/windowsStartScript.txt @@ -0,0 +1,91 @@ +@rem +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem ${applicationName} startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=.\ + +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME%${appHomeRelativePath} + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script. +set DEFAULT_JVM_OPTS=${defaultJvmOpts} + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=$classpath +<% if ( mainClassName.startsWith('--module ') ) { %>set MODULE_PATH=$modulePath<% } %> + +@rem Execute ${applicationName} +"%JAVA_EXE%" -javaagent:%APP_HOME%/lib/jacocoagent.jar %DEFAULT_JVM_OPTS% %JAVA_OPTS% %${optsEnvironmentVar}% <% if ( appNameSystemProperty ) { %>"-D${appNameSystemProperty}=%APP_BASE_NAME%"<% } %> -classpath "%CLASSPATH%" <% if ( mainClassName.startsWith('--module ') ) { %>--module-path "%MODULE_PATH%" <% } %>${mainClassName} %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable ${exitEnvironmentVar} if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%${exitEnvironmentVar}%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega