/* * Copyright 2022 Code Intelligence GmbH * * 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. */ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData; import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataReader; import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore; import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfoStore; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Test of coverage report and dump. * * Internally, JaCoCo is used to gather coverage information to guide the fuzzer to cover new * branches. This information can be dumped in the JaCoCo format and used to generate reports later * on. The dump only contains classes with at least one coverage data point. A JaCoCo report will * also include completely uncovered files based on the available classes in the stated jar files * in the report command. * * A human-readable coverage report can be generated directly by Jazzer. It contains information * on file level about all classes that should have been instrumented according to the * instrumentation_includes and instrumentation_exclude filters. */ @SuppressWarnings({"unused", "UnusedReturnValue"}) public final class CoverageFuzzer { // Not used during fuzz run, so not included in the dump public static class ClassNotToCover { private final int i; public ClassNotToCover(int i) { this.i = i; } public int getI() { return i; } } // Used in the fuzz run and included in the dump public static class ClassToCover { private final int i; public ClassToCover(int i) { if (i < 0 || i > 1000) { throw new IllegalArgumentException(String.format("Invalid repeat number \"%d\"", i)); } this.i = i; } public String repeat(String str) { if (str != null && str.length() >= 3 && str.length() <= 10) { return IntStream.range(0, i).mapToObj(i -> str).collect(Collectors.joining()); } throw new IllegalArgumentException(String.format("Invalid str \"%s\"", str)); } } public static void fuzzerTestOneInput(FuzzedDataProvider data) { try { ClassToCover classToCover = new ClassToCover(data.consumeInt()); String repeated = classToCover.repeat(data.consumeRemainingAsAsciiString()); if (repeated.equals("foofoofoo")) { throw new FuzzerSecurityIssueLow("Finished coverage fuzzer test"); } } catch (IllegalArgumentException ignored) { } } public static void fuzzerTearDown() throws IOException { assertCoverageReport(); assertCoverageDump(); } private static void assertCoverageReport() throws IOException { List coverage = Files.readAllLines(Paths.get(System.getenv("COVERAGE_REPORT_FILE"))); List> sections = new ArrayList<>(4); sections.add(new ArrayList<>()); coverage.forEach(l -> { if (l.isEmpty()) { sections.add(new ArrayList<>()); } else { sections.get(sections.size() - 1).add(l); } }); List branchCoverage = sections.get(0); assertEquals(2, branchCoverage.size()); List lineCoverage = sections.get(1); assertEquals(2, lineCoverage.size()); List incompleteCoverage = sections.get(2); assertEquals(2, incompleteCoverage.size()); List missedCoverage = sections.get(3); assertEquals(2, missedCoverage.size()); assertNotNull( branchCoverage.stream() .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) .findFirst() .orElseThrow(() -> new IllegalStateException("Could not find branch coverage"))); assertNotNull( lineCoverage.stream() .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) .findFirst() .orElseThrow(() -> new IllegalStateException("Could not find line coverage"))); assertNotNull( incompleteCoverage.stream() .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) .findFirst() .orElseThrow(() -> new IllegalStateException("Could not find incomplete coverage"))); String missed = missedCoverage.stream() .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) .findFirst() .orElseThrow(() -> new IllegalStateException("Could not find missed coverage")); List missingLines = IntStream.rangeClosed(63, 79) .mapToObj(i -> " " + i) .filter(missed::contains) .collect(Collectors.toList()); if (!missingLines.isEmpty()) { throw new IllegalStateException(String.format( "Missing coverage for ClassToCover on lines %s", String.join(", ", missingLines))); } } private static void assertCoverageDump() throws IOException { ExecutionDataStore executionDataStore = new ExecutionDataStore(); SessionInfoStore sessionInfoStore = new SessionInfoStore(); try (FileInputStream bais = new FileInputStream(System.getenv("COVERAGE_DUMP_FILE"))) { ExecutionDataReader reader = new ExecutionDataReader(bais); reader.setExecutionDataVisitor(executionDataStore); reader.setSessionInfoVisitor(sessionInfoStore); reader.read(); } assertEquals(2, executionDataStore.getContents().size()); ExecutionData coverageFuzzerCoverage = new ExecutionData(0, "", 0); ExecutionData classToCoverCoverage = new ExecutionData(0, "", 0); for (ExecutionData content : executionDataStore.getContents()) { if (content.getName().endsWith("ClassToCover")) { classToCoverCoverage = content; } else { coverageFuzzerCoverage = content; } } assertEquals("com/example/CoverageFuzzer", coverageFuzzerCoverage.getName()); assertEquals(7, countHits(coverageFuzzerCoverage.getProbes())); assertEquals("com/example/CoverageFuzzer$ClassToCover", classToCoverCoverage.getName()); assertEquals(11, countHits(classToCoverCoverage.getProbes())); } private static int countHits(boolean[] probes) { int count = 0; for (boolean probe : probes) { if (probe) count++; } return count; } private static void assertEquals(T expected, T actual) { if (!expected.equals(actual)) { throw new IllegalStateException( String.format("Expected \"%s\", got \"%s\"", expected, actual)); } } private static void assertNotNull(T actual) { if (actual == null) { throw new IllegalStateException("Expected none null value, got null"); } } }