From 5c5f864c518b198b1fca8ab09edd522ec6515ab0 Mon Sep 17 00:00:00 2001 From: yawkat Date: Tue, 7 Jan 2025 17:27:45 +0100 Subject: [PATCH] Add module for local jazzer runs --- build-logic/build.gradle | 5 + .../internal/tasks/GenerateModelClasses.java | 62 +++++- fuzzing-annotation-processor/build.gradle.kts | 2 - fuzzing-runner/build.gradle.kts | 16 ++ .../fuzzing/runner/LocalJazzerRunner.java | 198 ++++++++++++++++++ fuzzing-tests/build.gradle.kts | 1 + .../java/io/micronaut/fuzzing/TestTarget.java | 5 + .../fuzzing/http/EmbeddedHttpTarget.java | 5 + gradle/libs.versions.toml | 1 + jazzer-plugin/build.gradle.kts | 3 +- .../fuzzing/jazzer/BaseJazzerTask.java | 29 +-- .../fuzzing/jazzer/JazzerPlugin.java | 8 +- settings.gradle.kts | 1 + 13 files changed, 293 insertions(+), 43 deletions(-) create mode 100644 fuzzing-runner/build.gradle.kts create mode 100644 fuzzing-runner/src/main/java/io/micronaut/fuzzing/runner/LocalJazzerRunner.java diff --git a/build-logic/build.gradle b/build-logic/build.gradle index 9478b37..e03f6ad 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -2,6 +2,11 @@ plugins { id 'groovy-gradle-plugin' } +repositories { + mavenCentral() +} + dependencies { implementation(gradleApi()) + compileOnly("org.jetbrains:annotations:26.0.1") } diff --git a/build-logic/src/main/java/io/micronaut/build/internal/tasks/GenerateModelClasses.java b/build-logic/src/main/java/io/micronaut/build/internal/tasks/GenerateModelClasses.java index 962ecfc..fc8064b 100644 --- a/build-logic/src/main/java/io/micronaut/build/internal/tasks/GenerateModelClasses.java +++ b/build-logic/src/main/java/io/micronaut/build/internal/tasks/GenerateModelClasses.java @@ -21,6 +21,7 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.TaskAction; +import org.intellij.lang.annotations.Language; import java.io.IOException; import java.io.PrintWriter; @@ -43,18 +44,57 @@ public void generateModelClasses() throws IOException { try (var writer = new PrintWriter(Files.newBufferedWriter(model))) { writer.println("package " + packageName + ";"); writer.println(); - writer.println(""" - import java.util.List; - - public record DefinedFuzzTarget( - String targetClass, - List dictionary, - List dictionaryResources, - boolean enableImplicitly - ) { - public static final String DIRECTORY = "io.micronaut.fuzzing.fuzz-targets"; + @Language("java") + String s = """ +import java.io.OutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import io.micronaut.core.annotation.Internal; + +@Internal +public record DefinedFuzzTarget( + String targetClass, + List dictionary, + List dictionaryResources, + boolean enableImplicitly +) { + public static final String DIRECTORY = "io.micronaut.fuzzing.fuzz-targets"; + + public void writeStaticDictionary(OutputStream out) throws IOException { + if (dictionary() != null) { + out.write("# Manually defined dictionary entries\\n".getBytes(StandardCharsets.UTF_8)); + for (String s : dictionary()) { + out.write('"'); + for (byte b : s.getBytes(StandardCharsets.UTF_8)) { + if (b == '"' || b == '\\\\') { + // escape \\ and " + out.write('\\\\'); + out.write(b); + } else if (b >= ' ' && b <= '~') { + // printable ascii char + out.write((char) b); + } else { + out.write('\\\\'); + out.write('x'); + if ((b & 0xff) < 0x10) { + out.write('0'); + } + out.write(Integer.toHexString(b & 0xff).getBytes(StandardCharsets.UTF_8)); + } } - """); + out.write('"'); + out.write('\\n'); + } + } + } + + public static void writeResourceDictionaryPrefix(OutputStream out, String resourceName) throws IOException { + out.write(("# Dictionary from " + resourceName + "\\n").getBytes(StandardCharsets.UTF_8)); + } +} +"""; + writer.println(s.stripIndent()); } } } diff --git a/fuzzing-annotation-processor/build.gradle.kts b/fuzzing-annotation-processor/build.gradle.kts index fafb047..74f756c 100644 --- a/fuzzing-annotation-processor/build.gradle.kts +++ b/fuzzing-annotation-processor/build.gradle.kts @@ -1,5 +1,3 @@ -import io.micronaut.build.internal.tasks.GenerateModelClasses - plugins { id("io.micronaut.build.internal.fuzzing-module") id("io.micronaut.build.internal.fuzzing-model") diff --git a/fuzzing-runner/build.gradle.kts b/fuzzing-runner/build.gradle.kts new file mode 100644 index 0000000..b8bd6bf --- /dev/null +++ b/fuzzing-runner/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("io.micronaut.build.internal.fuzzing-module") + id("io.micronaut.build.internal.fuzzing-model") +} + +dependencies { + implementation(libs.managed.jazzer.standalone) + implementation(mn.jackson.databind) + implementation(projects.micronautFuzzingApi) +} + +tasks { + generateModel { + packageName = "io.micronaut.fuzzing.runner" + } +} diff --git a/fuzzing-runner/src/main/java/io/micronaut/fuzzing/runner/LocalJazzerRunner.java b/fuzzing-runner/src/main/java/io/micronaut/fuzzing/runner/LocalJazzerRunner.java new file mode 100644 index 0000000..b74f74e --- /dev/null +++ b/fuzzing-runner/src/main/java/io/micronaut/fuzzing/runner/LocalJazzerRunner.java @@ -0,0 +1,198 @@ +/* + * Copyright 2017-2025 original authors + * + * 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 + * + * https://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 io.micronaut.fuzzing.runner; + +import com.code_intelligence.jazzer.Jazzer; +import com.code_intelligence.jazzer.agent.AgentInstaller; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.core.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * This class can be used as a convenient runner for {@link io.micronaut.fuzzing.FuzzTarget}s. For + * example: + * + *
{@code
+ * @FuzzTarget
+ * public class Example {
+ *     public static void fuzzerTestOneInput(byte[] input) {
+ *         ...
+ *     }
+ *
+ *     public static void main(String[] args) {
+ *         LocalJazzerRunner.create(Example.class).fuzz();
+ *     }
+ * }
+ * }
+ * + * Running the main method will run the fuzzer. + */ +public final class LocalJazzerRunner { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final Class targetClass; + private final DefinedFuzzTarget target; + + private LocalJazzerRunner(Class targetClass, DefinedFuzzTarget target) { + this.targetClass = targetClass; + this.target = target; + } + + /** + * Create a runner for the given target class. + * + * @param fuzzTarget The fuzz target class. Must be annotated with {@link io.micronaut.fuzzing.FuzzTarget}. + * @return The runner + */ + @NonNull + public static LocalJazzerRunner create(@NonNull Class fuzzTarget) { + return new LocalJazzerRunner(fuzzTarget, findDefinition(fuzzTarget, fuzzTarget)); + } + + private void writeDictionary(OutputStream out) throws IOException { + target.writeStaticDictionary(out); + for (String r : target.dictionaryResources()) { + Enumeration urls = LocalJazzerRunner.class.getClassLoader().getResources(r); + if (!urls.hasMoreElements()) { + throw new IllegalStateException("Dictionary resource " + r + " not found"); + } + do { + DefinedFuzzTarget.writeResourceDictionaryPrefix(out, r); + try (InputStream in = urls.nextElement().openStream()) { + in.transferTo(out); + } + out.write('\n'); + } while (urls.hasMoreElements()); + } + } + + /** + * Run the normal jazzer fuzzer. + */ + public void fuzz() { + Path dict = null; + try { + List args = new ArrayList<>(); + if (target.dictionaryResources() != null || target.dictionary() != null) { + dict = Files.createTempFile("fuzzing-", ".dict"); + try (OutputStream out = Files.newOutputStream(dict)) { + writeDictionary(out); + } + args.add("-dict=" + dict.toAbsolutePath()); + } + args.add("--target_class=" + target.targetClass()); + + Jazzer.main(args.toArray(new String[0])); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + if (dict != null) { + try { + Files.deleteIfExists(dict); + } catch (IOException ignored) { + } + } + } + } + + /** + * Reproduce a crash with the given data in this JVM, for easy debugging. + * + * @param path Path to the data that leads to the crash + * @see #reproduce(byte[]) + */ + public void reproduce(@NonNull Path path) { + try { + reproduce(Files.readAllBytes(path)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Reproduce a crash with the given data in this JVM, for easy debugging. + * + * @param data The data that leads to the crash + */ + public void reproduce(byte @NonNull [] data) { + // this method somewhat based on jazzer's Replayer.java + + ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); + // this is needed to get some bootstrap classes (UnsafeProvider), but we don't actually need to instrument anything + AgentInstaller.install(false); + try { + try { + MethodHandles.lookup().findStatic(targetClass, "fuzzerInitialize", MethodType.methodType(void.class)) + .invoke(); + } catch (NoSuchMethodException ignored) { + } + try { + MethodHandles.lookup().findStatic(targetClass, "fuzzerTestOneInput", MethodType.methodType(void.class, byte[].class)) + .invokeExact(data); + } catch (NoSuchMethodException e) { + try { + MethodHandles.lookup().findStatic(targetClass, "fuzzerTestOneInput", MethodType.methodType(void.class, FuzzedDataProvider.class)) + .invokeExact((FuzzedDataProvider) FuzzedDataProviderImpl.withJavaData(data)); + } catch (NoSuchMethodException f) { + throw new IllegalArgumentException("Found no fuzzerTestOneInput method with appropriate argument type on " + targetClass, f); + } + } + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static DefinedFuzzTarget findDefinition(@NonNull Class ctx, @NonNull Class fuzzTarget) { + try (InputStream stream = LocalJazzerRunner.class.getResourceAsStream("/META-INF/" + DefinedFuzzTarget.DIRECTORY + "/" + ctx.getName() + ".json")) { + if (stream == null) { + if (ctx.getEnclosingClass() == null) { + throw new IllegalArgumentException("No fuzz target metadata found for " + fuzzTarget.getName() + ". Please make sure the target is annotated with @FuzzTarget, and that the annotation processor is applied."); + } + return findDefinition(ctx.getEnclosingClass(), fuzzTarget); + } + + List available = MAPPER.readValue(stream, new TypeReference<>() { + }); + for (DefinedFuzzTarget target : available) { + if (target.targetClass().equals(fuzzTarget.getName())) { + return target; + } + } + throw new IllegalArgumentException("No fuzz target metadata found for " + fuzzTarget.getName() + ", but other metadata is present. Please make sure the target is annotated with @FuzzTarget."); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/fuzzing-tests/build.gradle.kts b/fuzzing-tests/build.gradle.kts index 0ad0f1a..e1d218c 100644 --- a/fuzzing-tests/build.gradle.kts +++ b/fuzzing-tests/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(mnLogging.logback.classic) implementation(projects.micronautFuzzingApi) + implementation(projects.micronautFuzzingRunner) annotationProcessor(mn.micronaut.inject.java) annotationProcessor(projects.micronautFuzzingAnnotationProcessor) diff --git a/fuzzing-tests/src/main/java/io/micronaut/fuzzing/TestTarget.java b/fuzzing-tests/src/main/java/io/micronaut/fuzzing/TestTarget.java index 56be301..26ed35b 100644 --- a/fuzzing-tests/src/main/java/io/micronaut/fuzzing/TestTarget.java +++ b/fuzzing-tests/src/main/java/io/micronaut/fuzzing/TestTarget.java @@ -16,6 +16,7 @@ package io.micronaut.fuzzing; import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import io.micronaut.fuzzing.runner.LocalJazzerRunner; @FuzzTarget(enableImplicitly = false) public class TestTarget { @@ -29,4 +30,8 @@ public static void fuzzerTestOneInput(FuzzedDataProvider provider) { } } } + + public static void main(String[] args) { + LocalJazzerRunner.create(TestTarget.class).fuzz(); + } } diff --git a/fuzzing-tests/src/main/java/io/micronaut/fuzzing/http/EmbeddedHttpTarget.java b/fuzzing-tests/src/main/java/io/micronaut/fuzzing/http/EmbeddedHttpTarget.java index 115efd4..f1ad766 100644 --- a/fuzzing-tests/src/main/java/io/micronaut/fuzzing/http/EmbeddedHttpTarget.java +++ b/fuzzing-tests/src/main/java/io/micronaut/fuzzing/http/EmbeddedHttpTarget.java @@ -20,6 +20,7 @@ import io.micronaut.fuzzing.FlagAppender; import io.micronaut.fuzzing.FuzzTarget; import io.micronaut.fuzzing.HttpDict; +import io.micronaut.fuzzing.runner.LocalJazzerRunner; import io.micronaut.http.server.netty.NettyHttpServer; import io.micronaut.runtime.server.EmbeddedServer; import io.netty.buffer.ByteBuf; @@ -86,4 +87,8 @@ public static void fuzzerTestOneInput(byte[] input) { public static void fuzzerTearDown() { //CustomResourceLeakDetector.reportStillOpen(); } + + public static void main(String[] args) { + LocalJazzerRunner.create(EmbeddedHttpTarget.class).fuzz(); + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ad4e34..1e3e9e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ micronaut-logging = { module = "io.micronaut.logging:micronaut-logging-bom", ver # jdoctor = { module = "me.champeau.jdoctor:jdoctor-core", version.ref="jdoctor" } managed-jazzer-api = { module = 'com.code-intelligence:jazzer-api', version.ref = 'managed-jazzer' } +managed-jazzer-standalone = { module = 'com.code-intelligence:jazzer', version.ref = 'managed-jazzer' } [bundles] diff --git a/jazzer-plugin/build.gradle.kts b/jazzer-plugin/build.gradle.kts index 9579545..9681c4d 100644 --- a/jazzer-plugin/build.gradle.kts +++ b/jazzer-plugin/build.gradle.kts @@ -10,12 +10,13 @@ repositories { dependencies { implementation(mn.jackson.databind) + compileOnly(mn.micronaut.core) // annotations testImplementation(mnTest.junit.jupiter.api) testImplementation(mnTest.junit.jupiter.engine) } -tasks{ +tasks { test { useJUnitPlatform() } diff --git a/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/BaseJazzerTask.java b/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/BaseJazzerTask.java index da704b9..fcb9a58 100644 --- a/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/BaseJazzerTask.java +++ b/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/BaseJazzerTask.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitor; @@ -93,31 +92,7 @@ protected final List findFuzzTargets(ClasspathAccess cp) thro } protected final void buildDictionary(ClasspathAccess cp, OutputStream out, DefinedFuzzTarget target) throws IOException { - if (target.dictionary() != null) { - out.write("# Manually defined dictionary entries\n".getBytes(StandardCharsets.UTF_8)); - for (String s : target.dictionary()) { - out.write('"'); - for (byte b : s.getBytes(StandardCharsets.UTF_8)) { - if (b == '"' || b == '\\') { - // escape \ and " - out.write('\\'); - out.write(b); - } else if (b >= ' ' && b <= '~') { - // printable ascii char - out.write((char) b); - } else { - out.write('\\'); - out.write('x'); - if ((b & 0xff) < 0x10) { - out.write('0'); - } - out.write(Integer.toHexString(b & 0xff).getBytes(StandardCharsets.UTF_8)); - } - } - out.write('"'); - out.write('\n'); - } - } + target.writeStaticDictionary(out); if (target.dictionaryResources() != null) { for (String r : target.dictionaryResources()) { List resolved = cp.resolve(r); @@ -125,7 +100,7 @@ protected final void buildDictionary(ClasspathAccess cp, OutputStream out, Defin throw new IllegalStateException("Failed to find declared dictionary resource " + r + " for target " + target.targetClass()); } for (Path path : resolved) { - out.write(("# Dictionary from " + r + "\n").getBytes(StandardCharsets.UTF_8)); + DefinedFuzzTarget.writeResourceDictionaryPrefix(out, r); Files.copy(path, out); out.write('\n'); } diff --git a/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/JazzerPlugin.java b/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/JazzerPlugin.java index 3fce5d8..91c86e2 100644 --- a/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/JazzerPlugin.java +++ b/jazzer-plugin/src/main/java/io/micronaut/fuzzing/jazzer/JazzerPlugin.java @@ -12,6 +12,7 @@ import javax.inject.Inject; import java.io.File; +import java.util.Map; public abstract class JazzerPlugin implements Plugin { @Inject @@ -19,8 +20,11 @@ public abstract class JazzerPlugin implements Plugin { @Override public void apply(Project project) { - Configuration jazzerClasspath = project.getConfigurations().create("jazzerClasspath", c -> - c.extendsFrom(project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME))); + Configuration jazzerClasspath = project.getConfigurations().create("jazzerClasspath", c -> { + c.extendsFrom(project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); + // exclude jazzer, jazzer-api etc, since they are supplied by the runtime. + c.exclude(Map.of("group", "com.code-intelligence")); + }); jazzerClasspath.getDependencies().add(project.getDependencies().create(project)); project.getTasks().register("jazzer", JazzerTask.class, task -> { diff --git a/settings.gradle.kts b/settings.gradle.kts index 6caf325..da74cda 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ rootProject.name = "fuzzing-parent" include("fuzzing-annotation-processor") include("fuzzing-api") +include("fuzzing-runner") include("fuzzing-tests") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")