diff --git a/build.gradle b/build.gradle
index 6898086294..75471c17de 100644
--- a/build.gradle
+++ b/build.gradle
@@ -163,10 +163,6 @@ 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/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index bd4023f708..f9304c9f8e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -546,19 +546,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/gradle/versions.gradle b/gradle/versions.gradle
index 587b464ee3..f7599a8498 100644
--- a/gradle/versions.gradle
+++ b/gradle/versions.gradle
@@ -41,8 +41,6 @@ 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'
diff --git a/testfuzz/build.gradle b/testfuzz/build.gradle
index fe869209ee..3346f3e56d 100644
--- a/testfuzz/build.gradle
+++ b/testfuzz/build.gradle
@@ -39,7 +39,6 @@ dependencies {
implementation project(':util')
implementation 'com.fasterxml.jackson.core:jackson-databind'
- implementation 'com.gitlab.javafuzz:core'
implementation 'com.google.guava:guava'
implementation 'info.picocli:picocli'
implementation 'io.tmio:tuweni-bytes'
@@ -72,6 +71,15 @@ tasks.register("runFuzzer", JavaExec) {
}
}
+// Adds guidance to the fuzzer but with a 90% performance drop.
+tasks.register("fuzzGuided") {
+ doLast {
+ runFuzzer.args += "--guidance-regexp=org/(hyperledger/besu|apache/tuweni)"
+ runFuzzer.args += "--new-corpus-dir=${corpusDir}/.."
+ }
+ finalizedBy("runFuzzer")
+}
+
// This fuzzes besu as an external client. Besu fuzzing as a local client is enabled by default.
tasks.register("fuzzBesu") {
dependsOn(":installDist")
@@ -111,7 +119,7 @@ tasks.register("fuzzNethermind") {
tasks.register("fuzzReth") {
doLast {
- runFuzzer.args += "--client=revm=revme bytecode"
+ runFuzzer.args += "--client=revm=revme bytecode --eof-runtime"
}
finalizedBy("runFuzzer")
}
diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java
index c2b518f182..27c4547d26 100644
--- a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java
+++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java
@@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.referencetests.EOFTestCaseSpec;
import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.MainnetEVMs;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
+import org.hyperledger.besu.testfuzz.javafuzz.Fuzzer;
import java.io.File;
import java.io.FileOutputStream;
@@ -48,7 +49,6 @@ 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 com.google.common.base.Stopwatch;
import org.apache.tuweni.bytes.Bytes;
import picocli.CommandLine;
@@ -61,7 +61,7 @@ import picocli.CommandLine.Option;
description = "Fuzzes EOF container parsing and validation",
mixinStandardHelpOptions = true,
versionProvider = VersionProvider.class)
-public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnable {
+public class EofContainerSubCommand implements Runnable {
static final String COMMAND_NAME = "eof-container";
@@ -100,6 +100,16 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
description = "Minimum number of fuzz tests before a time limit fuzz error can occur")
private long timeThresholdIterations = 2_000;
+ @Option(
+ names = {"--guidance-regexp"},
+ description = "Regexp for classes that matter for guidance metric")
+ private String guidanceRegexp;
+
+ @Option(
+ names = {"--new-corpus-dir"},
+ description = "Directory to write hex versions of guidance added contracts")
+ private File newCorpusDir = null;
+
@CommandLine.ParentCommand private final BesuFuzzCommand parentCommand;
static final ObjectMapper eofTestMapper = createObjectMapper();
@@ -174,7 +184,13 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
System.out.println("Fuzzing client set: " + clients.keySet());
try {
- new Fuzzer(this, corpusDir.toString(), this::fuzzStats).start();
+ new Fuzzer(
+ this::parseEOFContainers,
+ corpusDir.toString(),
+ this::fuzzStats,
+ guidanceRegexp,
+ newCorpusDir)
+ .start();
} catch (NoSuchAlgorithmException
| ClassNotFoundException
| InvocationTargetException
@@ -212,8 +228,7 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
}
}
- @Override
- public void fuzz(final byte[] bytes) {
+ void parseEOFContainers(final byte[] bytes) {
Bytes eofUnderTest = Bytes.wrap(bytes);
String eofUnderTestHexString = eofUnderTest.toHexString();
@@ -236,7 +251,7 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
"%s: slow validation %d µs%n", client.getName(), elapsedMicros);
try {
Files.writeString(
- Path.of("slow-" + client.getName() + "-" + name + ".hex"),
+ Path.of("slow-" + name + "-" + client.getName() + ".hex"),
eofUnderTestHexString);
} catch (IOException e) {
throw new RuntimeException(e);
diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Corpus.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Corpus.java
new file mode 100644
index 0000000000..56eba76d37
--- /dev/null
+++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Corpus.java
@@ -0,0 +1,449 @@
+/*
+ * 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.javafuzz;
+
+import org.hyperledger.besu.crypto.MessageDigestFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Ported from ... because
+ * fields like max input size were not configurable.
+ */
+@SuppressWarnings("CatchAndPrintStackTrace")
+public class Corpus {
+ private final ArrayList inputs;
+ private final int maxInputSize;
+ private static final int[] INTERESTING8 = {-128, -1, 0, 1, 16, 32, 64, 100, 127};
+ private static final int[] INTERESTING16 = {
+ -32768, -129, 128, 255, 256, 512, 1000, 1024, 4096, 32767, -128, -1, 0, 1, 16, 32, 64, 100, 127
+ };
+ private static final int[] INTERESTING32 = {
+ -2147483648,
+ -100663046,
+ -32769,
+ 32768,
+ 65535,
+ 65536,
+ 100663045,
+ 2147483647,
+ -32768,
+ -129,
+ 128,
+ 255,
+ 256,
+ 512,
+ 1000,
+ 1024,
+ 4096,
+ 32767,
+ -128,
+ -1,
+ 0,
+ 1,
+ 16,
+ 32,
+ 64,
+ 100,
+ 127
+ };
+ private String corpusPath;
+ private int seedLength;
+
+ /**
+ * Create a corpus
+ *
+ * @param dirs The directory to store the corpus files
+ */
+ public Corpus(final String dirs) {
+ this.maxInputSize = 0xc001; // 48k+1
+ this.corpusPath = null;
+ this.inputs = new ArrayList<>();
+ if (dirs != null) {
+ String[] arr = dirs.split(",", -1);
+ for (String s : arr) {
+ File f = new File(s);
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ if (f.isDirectory()) {
+ if (this.corpusPath == null) {
+ this.corpusPath = f.getPath();
+ }
+ this.loadDir(f);
+ } else {
+ try {
+ this.inputs.add(Files.readAllBytes(f.toPath()));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ this.seedLength = this.inputs.size();
+ }
+
+ int getLength() {
+ return this.inputs.size();
+ }
+
+ private boolean randBool() {
+ return ThreadLocalRandom.current().nextBoolean();
+ }
+
+ private int rand(final int max) {
+ return ThreadLocalRandom.current().nextInt(0, max);
+ }
+
+ private void loadDir(final File dir) {
+ for (final File f : dir.listFiles()) {
+ if (f.isFile()) {
+ try {
+ this.inputs.add(Files.readAllBytes(f.toPath()));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ byte[] generateInput() throws NoSuchAlgorithmException {
+ if (this.seedLength != 0) {
+ this.seedLength--;
+ return this.inputs.get(this.seedLength);
+ }
+ if (this.inputs.isEmpty()) {
+ byte[] buf = new byte[] {};
+ this.putBuffer(buf);
+ return buf;
+ }
+ byte[] buf = this.inputs.get(this.rand(this.inputs.size()));
+ return this.mutate(buf);
+ }
+
+ void putBuffer(final byte[] buf) throws NoSuchAlgorithmException {
+ if (this.inputs.contains(buf)) {
+ return;
+ }
+
+ this.inputs.add(buf);
+
+ writeCorpusFile(buf);
+ }
+
+ private void writeCorpusFile(final byte[] buf) throws NoSuchAlgorithmException {
+ if (this.corpusPath != null) {
+ MessageDigest md = MessageDigestFactory.create("SHA-256");
+ md.update(buf);
+ byte[] digest = md.digest();
+ String hex = String.format("%064x", new BigInteger(1, digest));
+ try (FileOutputStream fos = new FileOutputStream(this.corpusPath + "/" + hex)) {
+ fos.write(buf);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private String dec2bin(final int dec) {
+ String bin = Integer.toBinaryString(dec);
+ String padding = new String(new char[32 - bin.length()]).replace("\0", "0");
+ return padding + bin;
+ }
+
+ private int exp2() {
+ String bin = dec2bin(this.rand((int) Math.pow(2, 32)));
+ int count = 0;
+ for (int i = 0; i < 32; i++) {
+ if (bin.charAt(i) == '0') {
+ count++;
+ } else {
+ break;
+ }
+ }
+ return count;
+ }
+
+ int chooseLen(final int n) {
+ int x = this.rand(100);
+ if (x < 90) {
+ return this.rand(Math.min(8, n)) + 1;
+ } else if (x < 99) {
+ return this.rand(Math.min(32, n)) + 1;
+ } else {
+ return this.rand(n) + 1;
+ }
+ }
+
+ static void copy(
+ final byte[] src, final int srcPos, final byte[] dst, final int dstPos, final int length) {
+ System.arraycopy(src, srcPos, dst, dstPos, Math.min(length, src.length - srcPos));
+ }
+
+ static void copy(final byte[] src, final int srcPos, final byte[] dst, final int dstPos) {
+ System.arraycopy(src, srcPos, dst, dstPos, Math.min(src.length - srcPos, dst.length - dstPos));
+ }
+
+ static byte[] concatZeros(final byte[] a, final int n) {
+ byte[] c = new byte[a.length + n];
+ Arrays.fill(c, (byte) 0);
+ System.arraycopy(a, 0, c, 0, a.length);
+ return c;
+ }
+
+ byte[] mutate(final byte[] buf) {
+ byte[] res = buf.clone();
+ int nm = 1 + this.exp2();
+ for (int i = 0; i < nm; i++) {
+ int x = this.rand(16);
+ if (x == 0) {
+ // Remove a range of bytes.
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int pos0 = this.rand(res.length);
+ int pos1 = pos0 + this.chooseLen(res.length - pos0);
+ copy(res, pos1, res, pos0, res.length - pos0);
+ res = Arrays.copyOfRange(res, 0, res.length - (pos1 - pos0));
+ } else if (x == 1) {
+ // Insert a range of random bytes.
+ int pos = this.rand(res.length + 1);
+ int n = this.chooseLen(10);
+ res = concatZeros(res, n);
+ copy(res, pos, res, pos + n);
+ for (int k = 0; k < n; k++) {
+ res[pos + k] = (byte) this.rand(256);
+ }
+ } else if (x == 2) {
+ // Duplicate a range of bytes.
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int src = this.rand(res.length);
+ int dst = this.rand(res.length);
+ while (src == dst) {
+ dst = this.rand(res.length);
+ }
+ int n = this.chooseLen(res.length - src);
+ byte[] tmp = new byte[n];
+ Arrays.fill(tmp, (byte) 0);
+ copy(res, src, tmp, 0);
+ res = concatZeros(res, n);
+ copy(res, dst, res, dst + n);
+ System.arraycopy(tmp, 0, res, dst, n);
+ } else if (x == 3) {
+ // Copy a range of bytes.
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int src = this.rand(res.length);
+ int dst = this.rand(res.length);
+ while (src == dst) {
+ dst = this.rand(res.length);
+ }
+ int n = this.chooseLen(res.length - src);
+ copy(res, src + n, res, dst);
+ } else if (x == 4) {
+ // Bit flip. Spooky!
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length);
+ res[pos] ^= (byte) (1 << (byte) this.rand(8));
+ } else if (x == 5) {
+ // Set a byte to a random value.
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length);
+ res[pos] ^= (byte) (this.rand(255) + 1);
+ } else if (x == 6) {
+ // Swap 2 bytes.
+ if (res.length <= 1) {
+ i--;
+ continue;
+ }
+ int src = this.rand(res.length);
+ int dst = this.rand(res.length);
+ while (src == dst) {
+ dst = this.rand(res.length);
+ }
+ byte tmp1 = res[src];
+ res[src] = res[dst];
+ res[dst] = tmp1;
+ } else if (x == 7) {
+ // Add/subtract from a byte.
+ if (res.length == 0) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length);
+ int v = this.rand(35) + 1;
+ if (this.randBool()) {
+ res[pos] += (byte) v;
+ } else {
+ res[pos] -= (byte) v;
+ }
+ } else if (x == 8) {
+ // Add/subtract from a uint16.
+ i--;
+ // if (res.length < 2) {
+ // i--;
+ // continue;
+ // }
+ // int pos = this.rand(res.length - 1);
+ // int v = this.rand(35) + 1;
+ // if (this.randBool()) {
+ // v = 0 - v;
+ // }
+ //
+ // if (this.randBool()) {
+ // res[pos] =
+ // } else {
+ //
+ // }
+
+ } else if (x == 9) {
+ i--;
+ // Add/subtract from a uint32.
+ } else if (x == 10) {
+ // Replace a byte with an interesting value.
+ if (res.length == 0) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length);
+ res[pos] = (byte) INTERESTING8[this.rand(INTERESTING8.length)];
+ } else if (x == 11) {
+ // Replace an uint16 with an interesting value.
+ if (res.length < 2) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length - 1);
+ if (this.randBool()) {
+ res[pos] = (byte) (INTERESTING16[this.rand(INTERESTING16.length)] & 0xFF);
+ res[pos + 1] = (byte) ((INTERESTING16[this.rand(INTERESTING16.length)] >> 8) & 0xFF);
+ } else {
+ res[pos + 1] = (byte) (INTERESTING16[this.rand(INTERESTING16.length)] & 0xFF);
+ res[pos] = (byte) ((INTERESTING16[this.rand(INTERESTING16.length)] >> 8) & 0xFF);
+ }
+ } else if (x == 12) {
+ // Replace an uint32 with an interesting value.
+ if (res.length < 4) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(res.length - 3);
+ if (this.randBool()) {
+ res[pos] = (byte) (INTERESTING32[this.rand(INTERESTING32.length)] & 0xFF);
+ res[pos + 1] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 8) & 0xFF);
+ res[pos + 2] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 16) & 0xFF);
+ res[pos + 3] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 24) & 0xFF);
+ } else {
+ res[pos + 3] = (byte) (INTERESTING32[this.rand(INTERESTING32.length)] & 0xFF);
+ res[pos + 2] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 8) & 0xFF);
+ res[pos + 1] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 16) & 0xFF);
+ res[pos] = (byte) ((INTERESTING32[this.rand(INTERESTING32.length)] >> 24) & 0xFF);
+ }
+ } else if (x == 13) {
+ // Replace an ascii digit with another digit.
+ List digits = new ArrayList<>();
+ for (int k = 0; k < res.length; k++) {
+ if (res[k] >= 48 && res[k] <= 57) {
+ digits.add(k);
+ }
+ }
+ if (digits.isEmpty()) {
+ i--;
+ continue;
+ }
+ int pos = this.rand(digits.size());
+ int was = res[digits.get(pos)];
+ int now = was;
+ while (now == was) {
+ now = this.rand(10) + 48;
+ }
+ res[digits.get(pos)] = (byte) now;
+ } else if (x == 14) {
+ // Splice another input.
+ if (res.length < 4 || this.inputs.size() < 2) {
+ i--;
+ continue;
+ }
+ byte[] other = this.inputs.get(this.rand(this.inputs.size()));
+ if (other.length < 4) {
+ i--;
+ continue;
+ }
+ // Find common prefix and suffix.
+ int idx0 = 0;
+ while (idx0 < res.length && idx0 < other.length && res[idx0] == other[idx0]) {
+ idx0++;
+ }
+ int idx1 = 0;
+ while (idx1 < res.length
+ && idx1 < other.length
+ && res[res.length - idx1 - 1] == other[other.length - idx1 - 1]) {
+ idx1++;
+ }
+ int diff = Math.min(res.length - idx0 - idx1, other.length - idx0 - idx1);
+ if (diff < 4) {
+ i--;
+ continue;
+ }
+ copy(other, idx0, res, idx0, Math.min(other.length, idx0 + this.rand(diff - 2) + 1) - idx0);
+ } else if (x == 15) {
+ // Insert a part of another input.
+ if (res.length < 4 || this.inputs.size() < 2) {
+ i--;
+ continue;
+ }
+ byte[] other = this.inputs.get(this.rand(this.inputs.size()));
+ if (other.length < 4) {
+ i--;
+ continue;
+ }
+ int pos0 = this.rand(res.length + 1);
+ int pos1 = this.rand(other.length - 2);
+ int n = this.chooseLen(other.length - pos1 - 2) + 2;
+ res = concatZeros(res, n);
+ copy(res, pos0, res, pos0 + n);
+ if (n >= 0) System.arraycopy(other, pos1, res, pos0, n);
+ }
+ }
+
+ if (res.length > this.maxInputSize) {
+ res = Arrays.copyOfRange(res, 0, this.maxInputSize);
+ }
+ return res;
+ }
+}
diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/FuzzTarget.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/FuzzTarget.java
new file mode 100644
index 0000000000..26f3522e11
--- /dev/null
+++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/FuzzTarget.java
@@ -0,0 +1,31 @@
+/*
+ * 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.javafuzz;
+
+/**
+ * Adapted from ... because
+ * I wanted it to be a functional interface
+ */
+@FunctionalInterface
+public interface FuzzTarget {
+
+ /**
+ * The target to fuzz
+ *
+ * @param data data proviced by the fuzzer
+ */
+ void fuzz(byte[] data);
+}
diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Fuzzer.java
similarity index 71%
rename from testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java
rename to testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Fuzzer.java
index e4ebb68cc8..f5d7f537a2 100644
--- a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/Fuzzer.java
+++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Fuzzer.java
@@ -12,14 +12,20 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
-package org.hyperledger.besu.testfuzz;
+package org.hyperledger.besu.testfuzz.javafuzz;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
import org.hyperledger.besu.crypto.Hash;
import org.hyperledger.besu.crypto.MessageDigestFactory;
+import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
@@ -28,9 +34,9 @@ import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
-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;
@@ -38,13 +44,19 @@ 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. */
+/**
+ * Ported from ... because
+ * JaCoCo APIs changed.
+ */
@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose
public class Fuzzer {
- private final AbstractFuzzTarget target;
+ private final FuzzTarget target;
private final Corpus corpus;
private final Object agent;
private final Method getExecutionDataMethod;
+ private final Pattern guidanceRegexp;
+ private final File newCorpusDir;
private long executionsInSample;
private long lastSampleTime;
private long totalExecutions;
@@ -58,6 +70,8 @@ public class 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
+ * @param guidanceRegexp Regexp of (slash delimited) class names to check for guidance.
+ * @param newCorpusDir Direcroty to dump hex encoded versions of guidance discovered tests.
* @throws ClassNotFoundException If Jacoco RT is not found (because jacocoagent.jar is not
* loaded)
* @throws NoSuchMethodException If the wrong version of Jacoco is loaded
@@ -66,7 +80,11 @@ public class Fuzzer {
* @throws NoSuchAlgorithmException If the SHA-256 crypto algo cannot be loaded.
*/
public Fuzzer(
- final AbstractFuzzTarget target, final String dirs, final Supplier fuzzStats)
+ final FuzzTarget target,
+ final String dirs,
+ final Supplier fuzzStats,
+ final String guidanceRegexp,
+ final File newCorpusDir)
throws ClassNotFoundException,
NoSuchMethodException,
InvocationTargetException,
@@ -75,6 +93,15 @@ public class Fuzzer {
this.target = target;
this.corpus = new Corpus(dirs);
this.fuzzStats = fuzzStats;
+ this.newCorpusDir = newCorpusDir;
+ if (newCorpusDir != null) {
+ if (newCorpusDir.exists() && newCorpusDir.isFile()) {
+ throw new IllegalArgumentException("New corpus directory already exists as a file");
+ }
+ newCorpusDir.mkdirs();
+ }
+ this.guidanceRegexp =
+ guidanceRegexp == null || guidanceRegexp.isBlank() ? null : Pattern.compile(guidanceRegexp);
Class> c = Class.forName("org.jacoco.agent.rt.RT");
Method getAgentMethod = c.getMethod("getAgent");
this.agent = getAgentMethod.invoke(null);
@@ -132,6 +159,11 @@ public class Fuzzer {
this.lastSampleTime = System.currentTimeMillis();
Map hitMap = new HashMap<>();
+ // preload some values so we don't get false hits in coverage we don't care about.
+ hitMap.put("org/hyperledger/besu/testfuzz/EofContainerSubCommand", 100);
+ hitMap.put("org/hyperledger/besu/testfuzz/Fuzzer", 100);
+ hitMap.put("org/hyperledger/besu/testfuzz/Fuzzer$HitCounter", 100);
+ hitMap.put("org/hyperledger/besu/testfuzz/InternalClient", 100);
while (true) {
byte[] buf = this.corpus.generateInput();
@@ -152,19 +184,22 @@ public class Fuzzer {
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);
- // }
+ if (totalExecutions > corpus.getLength()) {
+ this.logStats("NEW");
+ if (newCorpusDir != null) {
+
+ String filename = fileNameForBuffer(buf);
+ try (var pw =
+ new PrintWriter(
+ new BufferedWriter(
+ new OutputStreamWriter(
+ new FileOutputStream(new File(newCorpusDir, filename)), UTF_8)))) {
+ pw.println(Bytes.wrap(buf).toHexString());
+ } catch (IOException e) {
+ e.printStackTrace(System.out);
+ }
+ }
+ }
} else if ((System.currentTimeMillis() - this.lastSampleTime) > 30000) {
this.logStats("PULSE");
}
@@ -175,14 +210,17 @@ public class Fuzzer {
MessageDigest md = MessageDigestFactory.create(MessageDigestFactory.SHA256_ALG);
md.update(buf);
byte[] digest = md.digest();
- return String.format("./new-%064x.hex", new BigInteger(1, digest));
+ return String.format("new-%064x.hex", new BigInteger(1, digest));
}
private long getHitCount(final Map hitMap)
throws IllegalAccessException, InvocationTargetException {
+ if (guidanceRegexp == null) {
+ return 1;
+ }
byte[] dumpData = (byte[]) this.getExecutionDataMethod.invoke(this.agent, false);
ExecutionDataReader edr = new ExecutionDataReader(new ByteArrayInputStream(dumpData));
- HitCounter hc = new HitCounter(hitMap);
+ HitCounter hc = new HitCounter(hitMap, guidanceRegexp);
edr.setExecutionDataVisitor(hc);
edr.setSessionInfoVisitor(hc);
try {
@@ -198,28 +236,35 @@ public class Fuzzer {
static class HitCounter implements IExecutionDataVisitor, ISessionInfoVisitor {
long hits = 0;
Map hitMap;
+ Pattern guidanceRegexp;
- public HitCounter(final Map hitMap) {
+ public HitCounter(final Map hitMap, final Pattern guidanceRegexp) {
this.hitMap = hitMap;
+ this.guidanceRegexp = guidanceRegexp;
}
@Override
public void visitClassExecution(final ExecutionData executionData) {
+ String name = executionData.getName();
+ Matcher matcher = guidanceRegexp.matcher(name);
int hit = 0;
+ if (!matcher.find()) {
+ return;
+ }
+ if (matcher.start() != 0) {
+ return;
+ }
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) {
+ int theHits = hitMap.get(name);
+ if (theHits < hit) {
hitMap.put(name, hit);
+ } else {
+ hit = theHits;
}
} else {
hitMap.put(name, hit);
diff --git a/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/README.md b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/README.md
new file mode 100644
index 0000000000..276e404fa0
--- /dev/null
+++ b/testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/README.md
@@ -0,0 +1,14 @@
+## Credits & Acknowledgments
+
+These classes were ported from [Javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz/).
+Javafuzz is a port of [fuzzitdev/jsfuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz).
+
+Which in turn based based on [go-fuzz](https://github.com/dvyukov/go-fuzz) originally developed by [Dmitry Vyukov's](https://twitter.com/dvyukov).
+Which is in turn heavily based on [Michal Zalewski](https://twitter.com/lcamtuf) [AFL](http://lcamtuf.coredump.cx/afl/).
+
+## Changes
+
+* Increased max binary size to 48k+1
+* ported AbstractFuzzTarget to a functional interface FuzzTarget
+* Fixed some incompatibilities with JaCoCo
+* Besu style required changes (formatting, final params, etc.)
\ No newline at end of file