<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