From fa731020971f1a728b4b0e6667ddee9fe6caf239 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Sun, 1 Sep 2024 17:01:04 -0600 Subject: [PATCH] Performance improvements to EOF layout fuzzing (#7545) * Performance improvements to fuzzing Turning off guidance speeds the rate of testing up by 10%. Also, add other options to store new guided-discovered tests. Signed-off-by: Danno Ferrin * bring in the whole javafuzz lib so we can tweak it. Signed-off-by: Danno Ferrin --------- Signed-off-by: Danno Ferrin Co-authored-by: Sally MacFarlane --- build.gradle | 4 - gradle/verification-metadata.xml | 13 - gradle/versions.gradle | 2 - testfuzz/build.gradle | 12 +- .../besu/testfuzz/EofContainerSubCommand.java | 27 +- .../besu/testfuzz/javafuzz/Corpus.java | 449 ++++++++++++++++++ .../besu/testfuzz/javafuzz/FuzzTarget.java | 31 ++ .../besu/testfuzz/{ => javafuzz}/Fuzzer.java | 103 ++-- .../besu/testfuzz/javafuzz/README.md | 14 + 9 files changed, 599 insertions(+), 56 deletions(-) create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Corpus.java create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/FuzzTarget.java rename testfuzz/src/main/java/org/hyperledger/besu/testfuzz/{ => javafuzz}/Fuzzer.java (71%) create mode 100644 testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/README.md 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