mirror of
https://github.com/vacp2p/linea-besu.git
synced 2026-01-09 21:17:54 -05:00
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>
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
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 <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
|
||||
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<String> fuzzStats)
|
||||
final FuzzTarget target,
|
||||
final String dirs,
|
||||
final Supplier<String> 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<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) {
|
||||
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 (totalExecutions > corpus.getLength()) {
|
||||
this.logStats("NEW");
|
||||
if (newCorpusDir != null) {
|
||||
|
||||
// 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);
|
||||
// }
|
||||
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<String, Integer> 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<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.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);
|
||||
@@ -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.)
|
||||
Reference in New Issue
Block a user