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 <danno@numisight.com>

* bring in the whole javafuzz lib so we can tweak it.

Signed-off-by: Danno Ferrin <danno@numisight.com>

---------

Signed-off-by: Danno Ferrin <danno@numisight.com>
Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
pull/7551/head
Danno Ferrin 3 months ago committed by GitHub
parent 2339c1df20
commit fa73102097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      build.gradle
  2. 13
      gradle/verification-metadata.xml
  3. 2
      gradle/versions.gradle
  4. 12
      testfuzz/build.gradle
  5. 27
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java
  6. 449
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Corpus.java
  7. 31
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/FuzzTarget.java
  8. 103
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/Fuzzer.java
  9. 14
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/javafuzz/README.md

@ -163,10 +163,6 @@ allprojects {
url 'https://splunk.jfrog.io/splunk/ext-releases-local' url 'https://splunk.jfrog.io/splunk/ext-releases-local'
content { includeGroupByRegex('com\\.splunk\\..*') } content { includeGroupByRegex('com\\.splunk\\..*') }
} }
maven {
url 'https://gitlab.com/api/v4/projects/19871573/packages/maven'
content { includeGroupByRegex('com\\.gitlab\\.javafuzz(\\..*)?') }
}
mavenCentral() mavenCentral()

@ -546,19 +546,6 @@
<sha256 value="74da05b3ca50a8158101b7e12fbfbf902e011340f14bf31c1776cb51f96147f3" origin="Generated by Gradle"/> <sha256 value="74da05b3ca50a8158101b7e12fbfbf902e011340f14bf31c1776cb51f96147f3" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.gitlab.javafuzz" name="core" version="1.26">
<artifact name="core-1.26.jar">
<sha256 value="c6c2a7a67fac12db6dd495181082b2cc3fa8fd30399287854119054dde58ba92" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.26.pom">
<sha256 value="e218318c0edfea8c7f7030cbd2ffe9c7db206de39b16147d8a8a2a801515efd6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.gitlab.javafuzz" name="javafuzz" version="1.26">
<artifact name="javafuzz-1.26.pom">
<sha256 value="c5f521d9795c2bc11293ab08fbc563d453349b398b4fc5afe1388644abc392bf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google" name="google" version="5"> <component group="com.google" name="google" version="5">
<artifact name="google-5.pom"> <artifact name="google-5.pom">
<sha256 value="e09d345e73ca3fbca7f3e05f30deb74e9d39dd6b79a93fee8c511f23417b6828" origin="Generated by Gradle"/> <sha256 value="e09d345e73ca3fbca7f3e05f30deb74e9d39dd6b79a93fee8c511f23417b6828" origin="Generated by Gradle"/>

@ -41,8 +41,6 @@ dependencyManagement {
dependency 'org.hyperledger.besu:besu-errorprone-checks:1.0.0' 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.google.guava:guava:33.0.0-jre'
dependency 'com.graphql-java:graphql-java:21.5' dependency 'com.graphql-java:graphql-java:21.5'

@ -39,7 +39,6 @@ dependencies {
implementation project(':util') implementation project(':util')
implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.gitlab.javafuzz:core'
implementation 'com.google.guava:guava' implementation 'com.google.guava:guava'
implementation 'info.picocli:picocli' implementation 'info.picocli:picocli'
implementation 'io.tmio:tuweni-bytes' 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. // This fuzzes besu as an external client. Besu fuzzing as a local client is enabled by default.
tasks.register("fuzzBesu") { tasks.register("fuzzBesu") {
dependsOn(":installDist") dependsOn(":installDist")
@ -111,7 +119,7 @@ tasks.register("fuzzNethermind") {
tasks.register("fuzzReth") { tasks.register("fuzzReth") {
doLast { doLast {
runFuzzer.args += "--client=revm=revme bytecode" runFuzzer.args += "--client=revm=revme bytecode --eof-runtime"
} }
finalizedBy("runFuzzer") finalizedBy("runFuzzer")
} }

@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.referencetests.EOFTestCaseSpec;
import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.MainnetEVMs; import org.hyperledger.besu.evm.MainnetEVMs;
import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.testfuzz.javafuzz.Fuzzer;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; 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.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.gitlab.javafuzz.core.AbstractFuzzTarget;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes;
import picocli.CommandLine; import picocli.CommandLine;
@ -61,7 +61,7 @@ import picocli.CommandLine.Option;
description = "Fuzzes EOF container parsing and validation", description = "Fuzzes EOF container parsing and validation",
mixinStandardHelpOptions = true, mixinStandardHelpOptions = true,
versionProvider = VersionProvider.class) versionProvider = VersionProvider.class)
public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnable { public class EofContainerSubCommand implements Runnable {
static final String COMMAND_NAME = "eof-container"; 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") description = "Minimum number of fuzz tests before a time limit fuzz error can occur")
private long timeThresholdIterations = 2_000; 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; @CommandLine.ParentCommand private final BesuFuzzCommand parentCommand;
static final ObjectMapper eofTestMapper = createObjectMapper(); static final ObjectMapper eofTestMapper = createObjectMapper();
@ -174,7 +184,13 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
System.out.println("Fuzzing client set: " + clients.keySet()); System.out.println("Fuzzing client set: " + clients.keySet());
try { try {
new Fuzzer(this, corpusDir.toString(), this::fuzzStats).start(); new Fuzzer(
this::parseEOFContainers,
corpusDir.toString(),
this::fuzzStats,
guidanceRegexp,
newCorpusDir)
.start();
} catch (NoSuchAlgorithmException } catch (NoSuchAlgorithmException
| ClassNotFoundException | ClassNotFoundException
| InvocationTargetException | InvocationTargetException
@ -212,8 +228,7 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
} }
} }
@Override void parseEOFContainers(final byte[] bytes) {
public void fuzz(final byte[] bytes) {
Bytes eofUnderTest = Bytes.wrap(bytes); Bytes eofUnderTest = Bytes.wrap(bytes);
String eofUnderTestHexString = eofUnderTest.toHexString(); String eofUnderTestHexString = eofUnderTest.toHexString();
@ -236,7 +251,7 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
"%s: slow validation %d µs%n", client.getName(), elapsedMicros); "%s: slow validation %d µs%n", client.getName(), elapsedMicros);
try { try {
Files.writeString( Files.writeString(
Path.of("slow-" + client.getName() + "-" + name + ".hex"), Path.of("slow-" + name + "-" + client.getName() + ".hex"),
eofUnderTestHexString); eofUnderTestHexString);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);

@ -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 <a
* href="https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz">...</a> because
* fields like max input size were not configurable.
*/
@SuppressWarnings("CatchAndPrintStackTrace")
public class Corpus {
private final ArrayList<byte[]> 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<Integer> 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;
}
}

@ -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 <a
* href="https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz">...</a> 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);
}

@ -12,14 +12,20 @@
* *
* SPDX-License-Identifier: Apache-2.0 * 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.Hash;
import org.hyperledger.besu.crypto.MessageDigestFactory; import org.hyperledger.besu.crypto.MessageDigestFactory;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigInteger; import java.math.BigInteger;
@ -28,9 +34,9 @@ import java.security.NoSuchAlgorithmException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; 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.apache.tuweni.bytes.Bytes;
import org.jacoco.core.data.ExecutionData; import org.jacoco.core.data.ExecutionData;
import org.jacoco.core.data.ExecutionDataReader; 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.ISessionInfoVisitor;
import org.jacoco.core.data.SessionInfo; import org.jacoco.core.data.SessionInfo;
/** Ported from javafuzz because JaCoCo APIs changed. */ /**
* Ported from <a
* href="https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz">...</a> because
* JaCoCo APIs changed.
*/
@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose @SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose
public class Fuzzer { public class Fuzzer {
private final AbstractFuzzTarget target; private final FuzzTarget target;
private final Corpus corpus; private final Corpus corpus;
private final Object agent; private final Object agent;
private final Method getExecutionDataMethod; private final Method getExecutionDataMethod;
private final Pattern guidanceRegexp;
private final File newCorpusDir;
private long executionsInSample; private long executionsInSample;
private long lastSampleTime; private long lastSampleTime;
private long totalExecutions; private long totalExecutions;
@ -58,6 +70,8 @@ public class Fuzzer {
* @param target The target to fuzz * @param target The target to fuzz
* @param dirs the list of corpus dirs and files, comma separated. * @param dirs the list of corpus dirs and files, comma separated.
* @param fuzzStats additional fuzzing data from the client * @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 * @throws ClassNotFoundException If Jacoco RT is not found (because jacocoagent.jar is not
* loaded) * loaded)
* @throws NoSuchMethodException If the wrong version of Jacoco is 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. * @throws NoSuchAlgorithmException If the SHA-256 crypto algo cannot be loaded.
*/ */
public Fuzzer( public Fuzzer(
final AbstractFuzzTarget target, final String dirs, final Supplier<String> fuzzStats) final FuzzTarget target,
final String dirs,
final Supplier<String> fuzzStats,
final String guidanceRegexp,
final File newCorpusDir)
throws ClassNotFoundException, throws ClassNotFoundException,
NoSuchMethodException, NoSuchMethodException,
InvocationTargetException, InvocationTargetException,
@ -75,6 +93,15 @@ public class Fuzzer {
this.target = target; this.target = target;
this.corpus = new Corpus(dirs); this.corpus = new Corpus(dirs);
this.fuzzStats = fuzzStats; 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"); Class<?> c = Class.forName("org.jacoco.agent.rt.RT");
Method getAgentMethod = c.getMethod("getAgent"); Method getAgentMethod = c.getMethod("getAgent");
this.agent = getAgentMethod.invoke(null); this.agent = getAgentMethod.invoke(null);
@ -132,6 +159,11 @@ public class Fuzzer {
this.lastSampleTime = System.currentTimeMillis(); this.lastSampleTime = System.currentTimeMillis();
Map<String, Integer> hitMap = new HashMap<>(); Map<String, Integer> 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) { while (true) {
byte[] buf = this.corpus.generateInput(); byte[] buf = this.corpus.generateInput();
@ -152,19 +184,22 @@ public class Fuzzer {
if (newCoverage > this.totalCoverage) { if (newCoverage > this.totalCoverage) {
this.totalCoverage = newCoverage; this.totalCoverage = newCoverage;
this.corpus.putBuffer(buf); this.corpus.putBuffer(buf);
this.logStats("NEW"); if (totalExecutions > corpus.getLength()) {
this.logStats("NEW");
// If you want hex strings of new hits, uncomment the following. if (newCorpusDir != null) {
// String filename = fileNameForBuffer(buf);
// try (var pw = String filename = fileNameForBuffer(buf);
// new PrintWriter( try (var pw =
// new BufferedWriter( new PrintWriter(
// new OutputStreamWriter(new FileOutputStream(filename), UTF_8)))) { new BufferedWriter(
// pw.println(Bytes.wrap(buf).toHexString()); new OutputStreamWriter(
// System.out.println(filename); new FileOutputStream(new File(newCorpusDir, filename)), UTF_8)))) {
// } catch (IOException e) { pw.println(Bytes.wrap(buf).toHexString());
// e.printStackTrace(System.out); } catch (IOException e) {
// } e.printStackTrace(System.out);
}
}
}
} else if ((System.currentTimeMillis() - this.lastSampleTime) > 30000) { } else if ((System.currentTimeMillis() - this.lastSampleTime) > 30000) {
this.logStats("PULSE"); this.logStats("PULSE");
} }
@ -175,14 +210,17 @@ public class Fuzzer {
MessageDigest md = MessageDigestFactory.create(MessageDigestFactory.SHA256_ALG); MessageDigest md = MessageDigestFactory.create(MessageDigestFactory.SHA256_ALG);
md.update(buf); md.update(buf);
byte[] digest = md.digest(); 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<String, Integer> hitMap) private long getHitCount(final Map<String, Integer> hitMap)
throws IllegalAccessException, InvocationTargetException { throws IllegalAccessException, InvocationTargetException {
if (guidanceRegexp == null) {
return 1;
}
byte[] dumpData = (byte[]) this.getExecutionDataMethod.invoke(this.agent, false); byte[] dumpData = (byte[]) this.getExecutionDataMethod.invoke(this.agent, false);
ExecutionDataReader edr = new ExecutionDataReader(new ByteArrayInputStream(dumpData)); ExecutionDataReader edr = new ExecutionDataReader(new ByteArrayInputStream(dumpData));
HitCounter hc = new HitCounter(hitMap); HitCounter hc = new HitCounter(hitMap, guidanceRegexp);
edr.setExecutionDataVisitor(hc); edr.setExecutionDataVisitor(hc);
edr.setSessionInfoVisitor(hc); edr.setSessionInfoVisitor(hc);
try { try {
@ -198,28 +236,35 @@ public class Fuzzer {
static class HitCounter implements IExecutionDataVisitor, ISessionInfoVisitor { static class HitCounter implements IExecutionDataVisitor, ISessionInfoVisitor {
long hits = 0; long hits = 0;
Map<String, Integer> hitMap; Map<String, Integer> hitMap;
Pattern guidanceRegexp;
public HitCounter(final Map<String, Integer> hitMap) { public HitCounter(final Map<String, Integer> hitMap, final Pattern guidanceRegexp) {
this.hitMap = hitMap; this.hitMap = hitMap;
this.guidanceRegexp = guidanceRegexp;
} }
@Override @Override
public void visitClassExecution(final ExecutionData executionData) { public void visitClassExecution(final ExecutionData executionData) {
String name = executionData.getName();
Matcher matcher = guidanceRegexp.matcher(name);
int hit = 0; int hit = 0;
if (!matcher.find()) {
return;
}
if (matcher.start() != 0) {
return;
}
for (boolean b : executionData.getProbes()) { 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) { if (b) {
hit++; hit++;
} }
} }
String name = executionData.getName();
if (hitMap.containsKey(name)) { if (hitMap.containsKey(name)) {
if (hitMap.get(name) < hit) { int theHits = hitMap.get(name);
if (theHits < hit) {
hitMap.put(name, hit); hitMap.put(name, hit);
} else {
hit = theHits;
} }
} else { } else {
hitMap.put(name, hit); hitMap.put(name, hit);

@ -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.)
Loading…
Cancel
Save