Add slow parsing detection to EOF layout fuzzing (#7516)

* Add slow parsing validation

Add CLI flags and fuzzing logic to enable "slow" parsing to be a
loggable error.

* picocli final field issue

* fix some array boundary issues in pretty print and testing

Signed-off-by: Danno Ferrin <danno@numisight.com>
Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com>

---------

Signed-off-by: Danno Ferrin <danno@numisight.com>
Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com>
Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
pull/7530/head
Danno Ferrin 2 months ago committed by GitHub
parent c656ece8fc
commit c0e0103b2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/CodeValidateSubCommand.java
  2. 8
      ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/PrettyPrintSubCommand.java
  3. 40
      evm/src/main/java/org/hyperledger/besu/evm/code/EOFLayout.java
  4. 11
      testfuzz/build.gradle
  5. 109
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/EofContainerSubCommand.java
  6. 2
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/FuzzingClient.java
  7. 65
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/InternalClient.java
  8. 2
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/SingleQueryClient.java
  9. 2
      testfuzz/src/main/java/org/hyperledger/besu/testfuzz/StreamingClient.java

@ -126,10 +126,14 @@ public class CodeValidateSubCommand implements Runnable {
private void checkCodeFromBufferedReader(final BufferedReader in) {
try {
for (String code = in.readLine(); code != null; code = in.readLine()) {
try {
String validation = considerCode(code);
if (!Strings.isBlank(validation)) {
parentCommand.out.println(validation);
}
} catch (RuntimeException e) {
parentCommand.out.println("fail: " + e.getMessage());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
@ -151,14 +155,17 @@ public class CodeValidateSubCommand implements Runnable {
public String considerCode(final String hexCode) {
Bytes codeBytes;
try {
codeBytes =
Bytes.fromHexString(
hexCode.replaceAll("(^|\n)#[^\n]*($|\n)", "").replaceAll("[^0-9A-Za-z]", ""));
String strippedString =
hexCode.replaceAll("(^|\n)#[^\n]*($|\n)", "").replaceAll("[^0-9A-Za-z]", "");
if (Strings.isEmpty(strippedString)) {
return "";
}
codeBytes = Bytes.fromHexString(strippedString);
} catch (RuntimeException re) {
return "err: hex string -" + re;
}
if (codeBytes.isEmpty()) {
return "";
return "err: empty container";
}
EOFLayout layout = evm.get().parseEOF(codeBytes);

@ -89,7 +89,13 @@ public class PrettyPrintSubCommand implements Runnable {
LogConfigurator.setLevel("", "OFF");
for (var hexCode : codeList) {
Bytes container = Bytes.fromHexString(hexCode);
Bytes container;
try {
container = Bytes.fromHexString(hexCode);
} catch (IllegalArgumentException e) {
parentCommand.out.println("Invalid hex string: " + e.getMessage());
continue;
}
if (container.get(0) != ((byte) 0xef) && container.get(1) != 0) {
parentCommand.out.println(
"Pretty printing of legacy EVM is not supported. Patches welcome!");

@ -740,35 +740,59 @@ public record EOFLayout(
OpcodeInfo ci = V1_OPCODES[byteCode[pc] & 0xff];
if (ci.opcode() == RelativeJumpVectorOperation.OPCODE) {
if (byteCode.length <= pc + 1) {
out.printf(
" %02x # [%d] %s(<truncated instruction>)%n", byteCode[pc], pc, ci.name());
pc++;
} else {
int tableSize = byteCode[pc + 1] & 0xff;
out.printf("%02x%02x", byteCode[pc], byteCode[pc + 1]);
for (int j = 0; j <= tableSize; j++) {
out.printf("%02x%02x", byteCode[pc + j * 2 + 2], byteCode[pc + j * 2 + 3]);
int calculatedTableEnd = pc + tableSize * 2 + 4;
int lastTableEntry = Math.min(byteCode.length, calculatedTableEnd);
for (int j = pc + 2; j < lastTableEntry; j++) {
out.printf("%02x", byteCode[j]);
}
out.printf(" # [%d] %s(", pc, ci.name());
for (int j = 0; j <= tableSize; j++) {
if (j != 0) {
for (int j = pc + 3; j < lastTableEntry; j += 2) {
// j indexes to the second byte of the word, to handle mid-word truncation
if (j != pc + 3) {
out.print(',');
}
int b0 = byteCode[pc + j * 2 + 2]; // we want the sign extension, so no `& 0xff`
int b1 = byteCode[pc + j * 2 + 3] & 0xff;
int b0 = byteCode[j - 1]; // we want the sign extension, so no `& 0xff`
int b1 = byteCode[j] & 0xff;
out.print(b0 << 8 | b1);
}
if (byteCode.length < calculatedTableEnd) {
out.print("<truncated immediate>");
}
pc += tableSize * 2 + 4;
out.print(")\n");
}
} else if (ci.opcode() == RelativeJumpOperation.OPCODE
|| ci.opcode() == RelativeJumpIfOperation.OPCODE) {
if (pc + 1 >= byteCode.length) {
out.printf(" %02x # [%d] %s(<truncated immediate>)", byteCode[pc], pc, ci.name());
} else if (pc + 2 >= byteCode.length) {
out.printf(
" %02x%02x # [%d] %s(<truncated immediate>)",
byteCode[pc], byteCode[pc + 1], pc, ci.name());
} else {
int b0 = byteCode[pc + 1] & 0xff;
int b1 = byteCode[pc + 2] & 0xff;
short delta = (short) (b0 << 8 | b1);
out.printf("%02x%02x%02x # [%d] %s(%d)", byteCode[pc], b0, b1, pc, ci.name(), delta);
}
pc += 3;
out.printf("%n");
} else if (ci.opcode() == ExchangeOperation.OPCODE) {
if (pc + 1 >= byteCode.length) {
out.printf(" %02x # [%d] %s(<truncated immediate>)", byteCode[pc], pc, ci.name());
} else {
int imm = byteCode[pc + 1] & 0xff;
out.printf(
" %02x%02x # [%d] %s(%d, %d)",
byteCode[pc], imm, pc, ci.name(), imm >> 4, imm & 0x0F);
}
pc += 2;
out.printf("%n");
} else {
@ -784,7 +808,11 @@ public record EOFLayout(
}
out.printf(" # [%d] %s", pc, ci.name());
if (advance == 2) {
if (byteCode.length <= pc + 1) {
out.print("(<truncated immediate>)");
} else {
out.printf("(%d)", byteCode[pc + 1] & 0xff);
}
} else if (advance > 2) {
out.print("(0x");
for (int j = 1; j < advance && (pc + j) < byteCode.length; j++) {

@ -39,6 +39,7 @@ dependencies {
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'
implementation 'org.jacoco:org.jacoco.agent'
@ -56,6 +57,7 @@ application {
def corpusDir = "${buildDir}/generated/corpus"
tasks.register("runFuzzer", JavaExec) {
doNotTrackState("Produces no artifacts")
classpath = sourceSets.main.runtimeClasspath
mainClass = 'org.hyperledger.besu.testfuzz.BesuFuzz'
@ -69,6 +71,15 @@ tasks.register("runFuzzer", JavaExec) {
}
}
// This fuzzes besu as an external client. Besu fuzzing as a local client is enabled by default.
tasks.register("fuzzBesu") {
dependsOn(":installDist")
doLast {
runFuzzer.args += "--client=besu=../build/install/besu/bin/evmtool code-validate"
}
finalizedBy("runFuzzer")
}
tasks.register("fuzzEvmone") {
doLast {
runFuzzer.args += "--client=evm1=evmone-eofparse"

@ -17,14 +17,10 @@ package org.hyperledger.besu.testfuzz;
import static org.hyperledger.besu.testfuzz.EofContainerSubCommand.COMMAND_NAME;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.referencetests.EOFTestCaseSpec;
import org.hyperledger.besu.evm.Code;
import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.MainnetEVMs;
import org.hyperledger.besu.evm.code.CodeInvalid;
import org.hyperledger.besu.evm.code.CodeV1;
import org.hyperledger.besu.evm.code.EOFLayout;
import org.hyperledger.besu.evm.code.EOFLayout.EOFContainerMode;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import java.io.File;
@ -39,6 +35,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.util.DefaultIndenter;
@ -50,6 +49,7 @@ 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;
import picocli.CommandLine.Option;
@ -83,6 +83,23 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
description = "Add a client for differential fuzzing")
private final Map<String, String> clients = new LinkedHashMap<>();
@Option(
names = {"--no-local-client"},
description = "Don't include built-in Besu with fuzzing")
private final Boolean noLocalClient = false;
@Option(
names = {"--time-limit-ns"},
defaultValue = "5000",
description = "Time threshold, in nanoseconds, that results in a fuzz error if exceeded")
private long timeThresholdMicros = 5_000;
@Option(
names = {"--time-limit-warmup"},
defaultValue = "2000",
description = "Minimum number of fuzz tests before a time limit fuzz error can occur")
private long timeThresholdIterations = 2_000;
@CommandLine.ParentCommand private final BesuFuzzCommand parentCommand;
static final ObjectMapper eofTestMapper = createObjectMapper();
@ -91,7 +108,7 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
.getTypeFactory()
.constructParametricType(Map.class, String.class, EOFTestCaseSpec.class);
List<ExternalClient> externalClients = new ArrayList<>();
List<FuzzingClient> fuzzingClients = new ArrayList<>();
EVM evm = MainnetEVMs.pragueEOF(EvmConfiguration.DEFAULT);
long validContainers;
long totalContainers;
@ -150,7 +167,10 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
}
}
clients.forEach((k, v) -> externalClients.add(new StreamingClient(k, v.split(" "))));
if (!noLocalClient) {
fuzzingClients.add(new InternalClient("this"));
}
clients.forEach((k, v) -> fuzzingClients.add(new StreamingClient(k, v.split(" "))));
System.out.println("Fuzzing client set: " + clients.keySet());
try {
@ -196,55 +216,54 @@ public class EofContainerSubCommand extends AbstractFuzzTarget implements Runnab
public void fuzz(final byte[] bytes) {
Bytes eofUnderTest = Bytes.wrap(bytes);
String eofUnderTestHexString = eofUnderTest.toHexString();
Code code = evm.getCodeUncached(eofUnderTest);
Map<String, String> results = new LinkedHashMap<>();
boolean mismatch = false;
for (var client : externalClients) {
AtomicBoolean passHappened = new AtomicBoolean(false);
AtomicBoolean failHappened = new AtomicBoolean(false);
Map<String, String> resultMap =
fuzzingClients.stream()
.parallel()
.map(
client -> {
Stopwatch stopwatch = Stopwatch.createStarted();
String value = client.differentialFuzz(eofUnderTestHexString);
results.put(client.getName(), value);
if (value == null || value.startsWith("fail: ")) {
mismatch = true; // if an external client fails, always report it as an error
}
stopwatch.stop();
long elapsedMicros = stopwatch.elapsed(TimeUnit.MICROSECONDS);
if (elapsedMicros > timeThresholdMicros
&& totalContainers > timeThresholdIterations) {
Hash name = Hash.hash(eofUnderTest);
parentCommand.out.printf(
"%s: slow validation %d µs%n", client.getName(), elapsedMicros);
try {
Files.writeString(
Path.of("slow-" + client.getName() + "-" + name + ".hex"),
eofUnderTestHexString);
} catch (IOException e) {
throw new RuntimeException(e);
}
boolean besuValid = false;
String besuReason;
if (!code.isValid()) {
besuReason = ((CodeInvalid) code).getInvalidReason();
} else if (code.getEofVersion() != 1) {
EOFLayout layout = EOFLayout.parseEOF(eofUnderTest);
if (layout.isValid()) {
besuReason = "Besu Parsing Error";
parentCommand.out.println(layout.version());
parentCommand.out.println(layout.invalidReason());
parentCommand.out.println(code.getEofVersion());
parentCommand.out.println(code.getClass().getName());
System.exit(1);
mismatch = true;
} else {
besuReason = layout.invalidReason();
}
} else if (EOFContainerMode.INITCODE.equals(
((CodeV1) code).getEofLayout().containerMode().get())) {
besuReason = "Code is initcode, not runtime";
if (value.toLowerCase(Locale.ROOT).startsWith("ok")) {
passHappened.set(true);
} else if (value.toLowerCase(Locale.ROOT).startsWith("err")) {
failHappened.set(true);
} else {
besuReason = "OK";
besuValid = true;
// unexpected output: trigger a mismatch
passHappened.set(true);
failHappened.set(true);
}
for (var entry : results.entrySet()) {
mismatch =
mismatch
|| besuValid != entry.getValue().toUpperCase(Locale.getDefault()).startsWith("OK");
}
if (mismatch) {
parentCommand.out.println("besu: " + besuReason);
for (var entry : results.entrySet()) {
return Map.entry(client.getName(), value);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (passHappened.get() && failHappened.get()) {
for (var entry : resultMap.entrySet()) {
parentCommand.out.println(entry.getKey() + ": " + entry.getValue());
}
parentCommand.out.println("code: " + eofUnderTest.toUnprefixedHexString());
parentCommand.out.println("size: " + eofUnderTest.size());
parentCommand.out.println();
} else {
if (besuValid) {
if (passHappened.get()) {
validContainers++;
}
totalContainers++;

@ -14,7 +14,7 @@
*/
package org.hyperledger.besu.testfuzz;
interface ExternalClient {
interface FuzzingClient {
String getName();

@ -0,0 +1,65 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.testfuzz;
import org.hyperledger.besu.evm.Code;
import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.MainnetEVMs;
import org.hyperledger.besu.evm.code.CodeInvalid;
import org.hyperledger.besu.evm.code.CodeV1;
import org.hyperledger.besu.evm.code.EOFLayout;
import org.hyperledger.besu.evm.code.EOFLayout.EOFContainerMode;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.apache.tuweni.bytes.Bytes;
class InternalClient implements FuzzingClient {
String name;
final EVM evm;
public InternalClient(final String clientName) {
this.name = clientName;
this.evm = MainnetEVMs.pragueEOF(EvmConfiguration.DEFAULT);
}
@Override
public String getName() {
return name;
}
@Override
@SuppressWarnings("java:S2142")
public String differentialFuzz(final String data) {
try {
Bytes clientData = Bytes.fromHexString(data);
Code code = evm.getCodeUncached(clientData);
if (code.getEofVersion() < 1) {
return "err: legacy EVM";
} else if (!code.isValid()) {
return "err: " + ((CodeInvalid) code).getInvalidReason();
} else {
EOFLayout layout = ((CodeV1) code).getEofLayout();
if (EOFContainerMode.INITCODE.equals(layout.containerMode().get())) {
return "err: initcode container when runtime mode expected";
}
return "OK %d/%d/%d"
.formatted(
layout.getCodeSectionCount(), layout.getSubcontainerCount(), layout.dataLength());
}
} catch (RuntimeException e) {
return "fail: " + e.getMessage();
}
}
}

@ -24,7 +24,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings({"java:S106", "CallToPrintStackTrace"}) // we use lots the console, on purpose
class SingleQueryClient implements ExternalClient {
class SingleQueryClient implements FuzzingClient {
final String name;
String[] command;
Pattern okRegexp;

@ -19,7 +19,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
class StreamingClient implements ExternalClient {
class StreamingClient implements FuzzingClient {
final String name;
final BufferedReader reader;
final PrintWriter writer;

Loading…
Cancel
Save