1 // Copyright 2022 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.code_intelligence.jazzer.instrumentor;
16 
17 import java.io.ByteArrayOutputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.lang.invoke.MethodHandle;
21 import java.lang.invoke.MethodHandles;
22 import java.lang.invoke.MethodType;
23 import org.openjdk.jmh.annotations.Benchmark;
24 import org.openjdk.jmh.annotations.Scope;
25 import org.openjdk.jmh.annotations.Setup;
26 import org.openjdk.jmh.annotations.State;
27 
28 /**
29  * This benchmark compares the throughput of a typical fuzz target when instrumented with different
30  * edge coverage instrumentation strategies and coverage map implementations.
31  *
32  * The benchmark currently uses the OWASP json-sanitizer as its target, which has the following
33  * desirable properties for a benchmark:
34  * - It is a reasonably sized project that does not consist of many different classes.
35  * - It is very heavy on computation with a high density of branching.
36  * - It is entirely CPU bound with no IO and does not call expensive methods from the standard
37  *   library.
38  * With these properties, results obtained from this benchmark should provide reasonable lower
39  * bounds on the relative slowdown introduced by the various approaches to instrumentations.
40  */
41 @State(Scope.Benchmark)
42 public class CoverageInstrumentationBenchmark {
43   private static final String TARGET_CLASSNAME = "com.google.json.JsonSanitizer";
44   private static final String TARGET_PACKAGE =
45       TARGET_CLASSNAME.substring(0, TARGET_CLASSNAME.lastIndexOf('.'));
46   private static final String TARGET_METHOD = "sanitize";
47   private static final MethodType TARGET_TYPE = MethodType.methodType(String.class, String.class);
48 
49   // This is part of the benchmark's state and not a constant to prevent constant folding.
50   String TARGET_ARG =
51       "{\"foo\":1123987,\"bar\":[true, false],\"baz\":{\"foo\":\"132ä3\",\"bar\":1.123e-005}}";
52 
53   MethodHandle uninstrumented_sanitize;
54   MethodHandle local_DirectByteBuffer_NeverZero_sanitize;
55   MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize;
56   MethodHandle staticMethod_DirectByteBuffer2_NeverZero_sanitize;
57   MethodHandle staticMethod_Unsafe_NeverZero_sanitize;
58   MethodHandle staticMethod_Unsafe_NeverZero2_sanitize;
59   MethodHandle staticMethod_Unsafe_NeverZeroBranchfree_sanitize;
60   MethodHandle staticMethod_Unsafe_SimpleIncrement_sanitize;
61 
handleForTargetMethod(ClassLoader classLoader)62   public static MethodHandle handleForTargetMethod(ClassLoader classLoader)
63       throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
64     Class<?> targetClass = classLoader.loadClass(TARGET_CLASSNAME);
65     return MethodHandles.lookup().findStatic(targetClass, TARGET_METHOD, TARGET_TYPE);
66   }
67 
instrumentWithStrategy( EdgeCoverageStrategy strategy, Class<?> coverageMapClass)68   public static MethodHandle instrumentWithStrategy(
69       EdgeCoverageStrategy strategy, Class<?> coverageMapClass)
70       throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
71     if (strategy == null) {
72       // Do not instrument the code by using the benchmark class' ClassLoader.
73       return handleForTargetMethod(CoverageInstrumentationBenchmark.class.getClassLoader());
74     }
75     // It's fine to reuse a single instrumentor here as we don't want to know which class received
76     // how many counters.
77     Instrumentor instrumentor = new EdgeCoverageInstrumentor(strategy, coverageMapClass, 0);
78     return handleForTargetMethod(new InstrumentingClassLoader(instrumentor, TARGET_PACKAGE));
79   }
80 
81   @Setup
instrumentWithStrategies()82   public void instrumentWithStrategies()
83       throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
84     uninstrumented_sanitize = instrumentWithStrategy(null, null);
85     local_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy(
86         DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class);
87     staticMethod_DirectByteBuffer_NeverZero_sanitize =
88         instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBufferCoverageMap.class);
89     staticMethod_DirectByteBuffer2_NeverZero_sanitize =
90         instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBuffer2CoverageMap.class);
91     staticMethod_Unsafe_NeverZero_sanitize =
92         instrumentWithStrategy(new StaticMethodStrategy(), UnsafeCoverageMap.class);
93     staticMethod_Unsafe_NeverZero2_sanitize =
94         instrumentWithStrategy(new StaticMethodStrategy(), Unsafe2CoverageMap.class);
95     staticMethod_Unsafe_SimpleIncrement_sanitize =
96         instrumentWithStrategy(new StaticMethodStrategy(), UnsafeSimpleIncrementCoverageMap.class);
97     staticMethod_Unsafe_NeverZeroBranchfree_sanitize =
98         instrumentWithStrategy(new StaticMethodStrategy(), UnsafeBranchfreeCoverageMap.class);
99   }
100 
101   @Benchmark
uninstrumented()102   public String uninstrumented() throws Throwable {
103     return (String) uninstrumented_sanitize.invokeExact(TARGET_ARG);
104   }
105 
106   @Benchmark
local_DirectByteBuffer_NeverZero()107   public String local_DirectByteBuffer_NeverZero() throws Throwable {
108     return (String) local_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG);
109   }
110 
111   @Benchmark
staticMethod_DirectByteBuffer_NeverZero()112   public String staticMethod_DirectByteBuffer_NeverZero() throws Throwable {
113     return (String) staticMethod_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG);
114   }
115 
116   @Benchmark
staticMethod_DirectByteBuffer2_NeverZero()117   public String staticMethod_DirectByteBuffer2_NeverZero() throws Throwable {
118     return (String) staticMethod_DirectByteBuffer2_NeverZero_sanitize.invokeExact(TARGET_ARG);
119   }
120 
121   @Benchmark
staticMethod_Unsafe_NeverZero()122   public String staticMethod_Unsafe_NeverZero() throws Throwable {
123     return (String) staticMethod_Unsafe_NeverZero_sanitize.invokeExact(TARGET_ARG);
124   }
125 
126   @Benchmark
staticMethod_Unsafe_NeverZero2()127   public String staticMethod_Unsafe_NeverZero2() throws Throwable {
128     return (String) staticMethod_Unsafe_NeverZero2_sanitize.invokeExact(TARGET_ARG);
129   }
130 
131   @Benchmark
staticMethod_Unsafe_SimpleIncrement()132   public String staticMethod_Unsafe_SimpleIncrement() throws Throwable {
133     return (String) staticMethod_Unsafe_SimpleIncrement_sanitize.invokeExact(TARGET_ARG);
134   }
135 
136   @Benchmark
staticMethod_Unsafe_NeverZeroBranchfree()137   public String staticMethod_Unsafe_NeverZeroBranchfree() throws Throwable {
138     return (String) staticMethod_Unsafe_NeverZeroBranchfree_sanitize.invokeExact(TARGET_ARG);
139   }
140 }
141 
142 class InstrumentingClassLoader extends ClassLoader {
143   private final Instrumentor instrumentor;
144   private final String classNamePrefix;
145 
InstrumentingClassLoader(Instrumentor instrumentor, String packageToInstrument)146   InstrumentingClassLoader(Instrumentor instrumentor, String packageToInstrument) {
147     super(InstrumentingClassLoader.class.getClassLoader());
148     this.instrumentor = instrumentor;
149     this.classNamePrefix = packageToInstrument + ".";
150   }
151 
152   @Override
loadClass(String name)153   public Class<?> loadClass(String name) throws ClassNotFoundException {
154     if (!name.startsWith(classNamePrefix)) {
155       return super.loadClass(name);
156     }
157     try (InputStream stream = super.getResourceAsStream(name.replace('.', '/') + ".class")) {
158       if (stream == null) {
159         throw new ClassNotFoundException(String.format("Failed to find class file for %s", name));
160       }
161       byte[] bytecode = readAllBytes(stream);
162       byte[] instrumentedBytecode = instrumentor.instrument(name.replace('.', '/'), bytecode);
163       return defineClass(name, instrumentedBytecode, 0, instrumentedBytecode.length);
164     } catch (IOException e) {
165       throw new ClassNotFoundException(String.format("Failed to read class file for %s", name), e);
166     }
167   }
168 
readAllBytes(InputStream in)169   private static byte[] readAllBytes(InputStream in) throws IOException {
170     ByteArrayOutputStream out = new ByteArrayOutputStream();
171     byte[] buffer = new byte[64 * 104 * 1024];
172     int read;
173     while ((read = in.read(buffer)) != -1) {
174       out.write(buffer, 0, read);
175     }
176     return out.toByteArray();
177   }
178 }
179