• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 // Copyright 2021 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.agent
16 
17 import com.code_intelligence.jazzer.driver.Opt
18 import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor
19 import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
20 import com.code_intelligence.jazzer.instrumentor.Hook
21 import com.code_intelligence.jazzer.instrumentor.InstrumentationType
22 import com.code_intelligence.jazzer.utils.ClassNameGlobber
23 import com.code_intelligence.jazzer.utils.Log
24 import io.github.classgraph.ClassGraph
25 import java.io.File
26 import java.lang.instrument.ClassFileTransformer
27 import java.lang.instrument.Instrumentation
28 import java.nio.file.Path
29 import java.security.ProtectionDomain
30 import kotlin.math.roundToInt
31 import kotlin.system.exitProcess
32 import kotlin.time.measureTimedValue
33 
34 class RuntimeInstrumentor(
35     private val instrumentation: Instrumentation,
36     private val classesToFullyInstrument: ClassNameGlobber,
37     private val classesToHookInstrument: ClassNameGlobber,
38     private val instrumentationTypes: Set<InstrumentationType>,
39     private val includedHooks: List<Hook>,
40     private val customHooks: List<Hook>,
41     // Dedicated name globber for additional classes to hook stated in hook annotations is needed due to
42     // existing include and exclude pattern of classesToHookInstrument. All classes are included in hook
43     // instrumentation except the ones from default excludes, like JDK and Kotlin classes. But additional
44     // classes to hook, based on annotations, are allowed to reference normally ignored ones, like JDK
45     // and Kotlin internals.
46     // FIXME: Adding an additional class to hook will apply _all_ hooks to it and not only the one it's
47     // defined in. At some point we might want to track the list of classes per custom hook rather than globally.
48     private val additionalClassesToHookInstrument: ClassNameGlobber,
49     private val coverageIdSynchronizer: CoverageIdStrategy,
50     private val dumpClassesDir: Path?,
51 ) : ClassFileTransformer {
52 
53     @kotlin.time.ExperimentalTime
54     override fun transform(
55         loader: ClassLoader?,
56         internalClassName: String,
57         classBeingRedefined: Class<*>?,
58         protectionDomain: ProtectionDomain?,
59         classfileBuffer: ByteArray,
60     ): ByteArray? {
61         var pathPrefix = ""
62         if (!Opt.instrumentOnly.isEmpty() && protectionDomain != null) {
63             var outputPathPrefix = protectionDomain.getCodeSource().getLocation().getFile().toString()
64             if (outputPathPrefix.isNotEmpty()) {
65                 if (outputPathPrefix.contains(File.separator)) {
66                     outputPathPrefix = outputPathPrefix.substring(outputPathPrefix.lastIndexOf(File.separator) + 1, outputPathPrefix.length)
67                 }
68 
69                 if (outputPathPrefix.endsWith(".jar")) {
70                     outputPathPrefix = outputPathPrefix.substring(0, outputPathPrefix.lastIndexOf(".jar"))
71                 }
72 
73                 if (outputPathPrefix.isNotEmpty()) {
74                     pathPrefix = outputPathPrefix + File.separator
75                 }
76             }
77         }
78 
79         return try {
80             // Bail out early if we would instrument ourselves. This prevents ClassCircularityErrors as we might need to
81             // load additional Jazzer classes until we reach the full exclusion logic.
82             if (internalClassName.startsWith("com/code_intelligence/jazzer/")) {
83                 return null
84             }
85             // Workaround for a JDK bug (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8299798):
86             // When retransforming a class in the Java standard library, the provided classfileBuffer does not contain
87             // any StackMapTable attributes. Our transformations require stack map frames to calculate the number of
88             // local variables and stack slots as well as when adding control flow.
89             //
90             // We work around this by reloading the class file contents if we are retransforming (classBeingRedefined
91             // is also non-null in this situation) and the class is provided by the bootstrap loader.
92             //
93             // Alternatives considered:
94             // Using ClassWriter.COMPUTE_FRAMES as an escape hatch isn't possible in the context of an agent as the
95             // computation may itself need to load classes, which leads to circular loads and incompatible class
96             // redefinitions.
97             transformInternal(internalClassName, classfileBuffer.takeUnless { loader == null && classBeingRedefined != null })
98         } catch (t: Throwable) {
99             // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation
100             // failures. The docs advise to use a top-level try-catch.
101             // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html
102             if (dumpClassesDir != null) {
103                 dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed", pathPrefix = pathPrefix)
104             }
105             Log.warn("Failed to instrument $internalClassName:", t)
106             throw t
107         }.also { instrumentedByteCode ->
108             // Only dump classes that were instrumented.
109             if (instrumentedByteCode != null && dumpClassesDir != null) {
110                 dumpToClassFile(internalClassName, instrumentedByteCode, pathPrefix = pathPrefix)
111                 dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".original", pathPrefix = pathPrefix)
112             }
113         }
114     }
115 
116     private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "", pathPrefix: String = "") {
117         val relativePath = "$pathPrefix$internalClassName$basenameSuffix.class"
118         val absolutePath = dumpClassesDir!!.resolve(relativePath)
119         val dumpFile = absolutePath.toFile()
120         dumpFile.parentFile.mkdirs()
121         dumpFile.writeBytes(bytecode)
122     }
123 
124     @kotlin.time.ExperimentalTime
125     override fun transform(
126         module: Module?,
127         loader: ClassLoader?,
128         internalClassName: String,
129         classBeingRedefined: Class<*>?,
130         protectionDomain: ProtectionDomain?,
131         classfileBuffer: ByteArray,
132     ): ByteArray? {
133         try {
134             if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) {
135                 // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the
136                 // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the
137                 // injected bytecode might throw NoClassDefFoundError.
138                 // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html
139                 if (!instrumentation.isModifiableModule(module)) {
140                     val prettyClassName = internalClassName.replace('/', '.')
141                     Log.warn("Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping")
142                     return null
143                 }
144                 instrumentation.redefineModule(
145                     module,
146                     setOf(RuntimeInstrumentor::class.java.module), // extraReads
147                     emptyMap(),
148                     emptyMap(),
149                     emptySet(),
150                     emptyMap(),
151                 )
152             }
153         } catch (t: Throwable) {
154             // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation
155             // failures. The docs advise to use a top-level try-catch.
156             // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html
157             if (dumpClassesDir != null) {
158                 dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed")
159             }
160             Log.warn("Failed to instrument $internalClassName:", t)
161             throw t
162         }
163         return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer)
164     }
165 
166     @kotlin.time.ExperimentalTime
167     fun transformInternal(internalClassName: String, maybeClassfileBuffer: ByteArray?): ByteArray? {
168         val (fullInstrumentation, printInfo) = when {
169             classesToFullyInstrument.includes(internalClassName) -> Pair(true, true)
170             classesToHookInstrument.includes(internalClassName) -> Pair(false, true)
171             // The classes to hook specified by hooks are more of an implementation detail of the hook. The list is
172             // always the same unless the set of hooks changes and doesn't help the user judge whether their classes are
173             // being instrumented, so we don't print info for them.
174             additionalClassesToHookInstrument.includes(internalClassName) -> Pair(false, false)
175             else -> return null
176         }
177         val className = internalClassName.replace('/', '.')
178         val classfileBuffer = maybeClassfileBuffer ?: ClassGraph()
179             .enableSystemJarsAndModules()
180             .ignoreClassVisibility()
181             .acceptClasses(className)
182             .scan()
183             .use {
184                 it.getClassInfo(className)?.resource?.load() ?: run {
185                     Log.warn("Failed to load bytecode of class $className")
186                     return null
187                 }
188             }
189         val (instrumentedBytecode, duration) = measureTimedValue {
190             try {
191                 instrument(internalClassName, classfileBuffer, fullInstrumentation)
192             } catch (e: CoverageIdException) {
193                 Log.error("Coverage IDs are out of sync")
194                 e.printStackTrace()
195                 exitProcess(1)
196             }
197         }
198         val durationInMs = duration.inWholeMilliseconds
199         val sizeIncrease = ((100.0 * (instrumentedBytecode.size - classfileBuffer.size)) / classfileBuffer.size).roundToInt()
200         if (printInfo) {
201             if (fullInstrumentation) {
202                 Log.info("Instrumented $className (took $durationInMs ms, size +$sizeIncrease%)")
203             } else {
204                 Log.info("Instrumented $className with custom hooks only (took $durationInMs ms, size +$sizeIncrease%)")
205             }
206         }
207         return instrumentedBytecode
208     }
209 
210     private fun instrument(internalClassName: String, bytecode: ByteArray, fullInstrumentation: Boolean): ByteArray {
211         val classWithHooksEnabledField = if (Opt.conditionalHooks) {
212             // Let the hook instrumentation emit additional logic that checks the value of the
213             // hooksEnabled field on this class and skips the hook if it is false.
214             "com/code_intelligence/jazzer/runtime/JazzerInternal"
215         } else {
216             null
217         }
218         return ClassInstrumentor(internalClassName, bytecode).run {
219             if (fullInstrumentation) {
220                 // Coverage instrumentation must be performed before any other code updates
221                 // or there will be additional coverage points injected if any calls are inserted
222                 // and JaCoCo will produce a broken coverage report.
223                 coverageIdSynchronizer.withIdForClass(internalClassName) { firstId ->
224                     coverage(firstId).also { actualNumEdgeIds ->
225                         CoverageRecorder.recordInstrumentedClass(
226                             internalClassName,
227                             bytecode,
228                             firstId,
229                             actualNumEdgeIds,
230                         )
231                     }
232                 }
233                 // Hook instrumentation must be performed after data flow tracing as the injected
234                 // bytecode would trigger the GEP callbacks for byte[].
235                 traceDataFlow(instrumentationTypes)
236                 hooks(includedHooks + customHooks, classWithHooksEnabledField)
237             } else {
238                 hooks(customHooks, classWithHooksEnabledField)
239             }
240             instrumentedBytecode
241         }
242     }
243 }
244