<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.instrumentor
16
17 import com.code_intelligence.jazzer.api.HookType
18 import org.objectweb.asm.Handle
19 import org.objectweb.asm.Label
20 import org.objectweb.asm.MethodVisitor
21 import org.objectweb.asm.Opcodes
22 import org.objectweb.asm.Type
23 import org.objectweb.asm.commons.AnalyzerAdapter
24 import org.objectweb.asm.commons.LocalVariablesSorter
25 import java.util.concurrent.atomic.AtomicBoolean
26
27 internal fun makeHookMethodVisitor(
28 owner: String,
29 access: Int,
30 name: String?,
31 descriptor: String?,
32 methodVisitor: MethodVisitor?,
33 hooks: Iterable<Hook>,
34 java6Mode: Boolean,
35 random: DeterministicRandom,
36 classWithHooksEnabledField: String?,
37 ): MethodVisitor {
38 return HookMethodVisitor(
39 owner,
40 access,
41 name,
42 descriptor,
43 methodVisitor,
44 hooks,
45 java6Mode,
46 random,
47 classWithHooksEnabledField,
48 ).lvs
49 }
50
51 private class HookMethodVisitor(
52 owner: String,
53 access: Int,
54 val name: String?,
55 descriptor: String?,
56 methodVisitor: MethodVisitor?,
57 hooks: Iterable<Hook>,
58 private val java6Mode: Boolean,
59 private val random: DeterministicRandom,
60 private val classWithHooksEnabledField: String?,
61 ) : MethodVisitor(
62 Instrumentor.ASM_API_VERSION,
63 // AnalyzerAdapter computes stack map frames at every instruction, which is needed for the
64 // conditional hook logic as it adds a conditional jump. Before Java 7, stack map frames were
65 // neither included nor required in class files.
66 //
67 // Note: Delegating to AnalyzerAdapter rather than having AnalyzerAdapter delegate to our
68 // MethodVisitor is unusual. We do this since we insert conditional jumps around method calls,
69 // which requires knowing the stack map both before and after the call. If AnalyzerAdapter
70 // delegated to this MethodVisitor, we would only be able to access the stack map before the
71 // method call in visitMethodInsn.
72 if (classWithHooksEnabledField != null && !java6Mode) {
73 AnalyzerAdapter(
74 owner,
75 access,
76 name,
77 descriptor,
78 methodVisitor,
79 )
80 } else {
81 methodVisitor
82 },
83 ) {
84
85 companion object {
86 private val showUnsupportedHookWarning = AtomicBoolean(true)
87 }
88
89 val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) {
updateNewLocalsnull90 override fun updateNewLocals(newLocals: Array<Any>) {
91 // The local variables involved in calling hooks do not need to outlive the current
92 // basic block and should thus not appear in stack map frames. By requesting the
93 // LocalVariableSorter to fill their entries in stack map frames with TOP, they will
94 // be treated like an unused local variable slot.
95 newLocals.fill(Opcodes.TOP)
96 }
97 }
98
hooknull99 private val hooks = hooks.groupBy { hook ->
100 var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}"
101 if (hook.targetMethodDescriptor != null) {
102 hookKey += "#${hook.targetMethodDescriptor}"
103 }
104 hookKey
105 }
106
visitMethodInsnnull107 override fun visitMethodInsn(
108 opcode: Int,
109 owner: String,
110 methodName: String,
111 methodDescriptor: String,
112 isInterface: Boolean,
113 ) {
114 if (!isMethodInvocationOp(opcode)) {
115 mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
116 return
117 }
118 handleMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
119 }
120
121 // Transforms a stack map specification from the form used by the JVM and AnalyzerAdapter, where
122 // LONG and DOUBLE values are followed by an additional TOP entry, to the form accepted by
123 // visitFrame, which doesn't expect this additional entry.
dropImplicitTopnull124 private fun dropImplicitTop(stack: Collection<Any>?): Array<Any>? {
125 if (stack == null) {
126 return null
127 }
128 val filteredStack = mutableListOf<Any>()
129 var previousElement: Any? = null
130 for (element in stack) {
131 if (element != Opcodes.TOP || (previousElement != Opcodes.DOUBLE && previousElement != Opcodes.LONG)) {
132 filteredStack.add(element)
133 }
134 previousElement = element
135 }
136 return filteredStack.toTypedArray()
137 }
138
storeFramenull139 private fun storeFrame(aa: AnalyzerAdapter?): Pair<Array<Any>?, Array<Any>?>? {
140 return Pair(dropImplicitTop((aa ?: return null).locals), dropImplicitTop(aa.stack))
141 }
142
handleMethodInsnnull143 fun handleMethodInsn(
144 opcode: Int,
145 owner: String,
146 methodName: String,
147 methodDescriptor: String,
148 isInterface: Boolean,
149 ) {
150 val matchingHooks = findMatchingHooks(owner, methodName, methodDescriptor)
151
152 if (matchingHooks.isEmpty()) {
153 mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
154 return
155 }
156
157 val skipHooksLabel = Label()
158 val applyHooksLabel = Label()
159 val useConditionalHooks = classWithHooksEnabledField != null
160 var postCallFrame: Pair<Array<Any>?, Array<Any>?>? = null
161 if (useConditionalHooks) {
162 val preCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) }
163 // If hooks aren't enabled, skip the hook invocations.
164 mv.visitFieldInsn(
165 Opcodes.GETSTATIC,
166 classWithHooksEnabledField,
167 "hooksEnabled",
168 "Z",
169 )
170 mv.visitJumpInsn(Opcodes.IFNE, applyHooksLabel)
171 mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
172 postCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) }
173 mv.visitJumpInsn(Opcodes.GOTO, skipHooksLabel)
174 // Needs a stack map frame as both the successor of an unconditional jump and the target
175 // of a jump.
176 mv.visitLabel(applyHooksLabel)
177 if (preCallFrame != null) {
178 mv.visitFrame(
179 Opcodes.F_NEW,
180 preCallFrame.first?.size ?: 0,
181 preCallFrame.first,
182 preCallFrame.second?.size ?: 0,
183 preCallFrame.second,
184 )
185 }
186 // All successor instructions emitted below do not have a stack map frame attached, so
187 // we do not need to emit a NOP to prevent duplicated stack map frames.
188 }
189
190 val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor)
191 val localObjArr = storeMethodArguments(paramDescriptors)
192 // If the method we're hooking is not static there is now a reference to
193 // the object the method was invoked on at the top of the stack.
194 // If the method is static, that object is missing. We make up for it by pushing a null ref.
195 if (opcode == Opcodes.INVOKESTATIC) {
196 mv.visitInsn(Opcodes.ACONST_NULL)
197 }
198
199 // Save the owner object to a new local variable
200 val ownerDescriptor = "L$owner;"
201 val localOwnerObj = lvs.newLocal(Type.getType(ownerDescriptor))
202 mv.visitVarInsn(Opcodes.ASTORE, localOwnerObj) // consume objectref
203 // We now removed all values for the original method call from the operand stack
204 // and saved them to local variables.
205
206 val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor)
207 // Create a local variable to store the return value
208 val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor)))
209
210 matchingHooks.forEachIndexed { index, hook ->
211 // The hookId is used to identify a call site.
212 val hookId = random.nextInt()
213
214 // Start to build the arguments for the hook method.
215 if (methodName == "<init>") {
216 // Constructor is invoked on an uninitialized object, and that's still on the stack.
217 // In case of REPLACE pop it from the stack and replace it afterwards with the returned
218 // one from the hook.
219 if (hook.hookType == HookType.REPLACE) {
220 mv.visitInsn(Opcodes.POP)
221 }
222 // Special case for constructors:
223 // We cannot create a MethodHandle for a constructor, so we push null instead.
224 mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
225 // Only pass the this object if it has been initialized by the time the hook is invoked.
226 if (hook.hookType == HookType.AFTER) {
227 mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
228 } else {
229 mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
230 }
231 } else {
232 // Push a MethodHandle representing the hooked method.
233 val handleOpcode = when (opcode) {
234 Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL
235 Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE
236 Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC
237 Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL
238 else -> -1
239 }
240 if (java6Mode) {
241 // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50).
242 mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
243 } else {
244 mv.visitLdcInsn(
245 Handle(
246 handleOpcode,
247 owner,
248 methodName,
249 methodDescriptor,
250 isInterface,
251 ),
252 ) // push MethodHandle
253 }
254 // Stack layout: ... | MethodHandle (objectref)
255 // Push the owner object again
256 mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
257 }
258 // Stack layout: ... | MethodHandle (objectref) | owner (objectref)
259 // Push a reference to our object array with the saved arguments
260 mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
261 // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref)
262 // Push the hook id
263 mv.visitLdcInsn(hookId)
264 // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
265 // How we proceed depends on the type of hook we want to implement
266 when (hook.hookType) {
267 HookType.BEFORE -> {
268 // Call the hook method
269 mv.visitMethodInsn(
270 Opcodes.INVOKESTATIC,
271 hook.hookInternalClassName,
272 hook.hookMethodName,
273 hook.hookMethodDescriptor,
274 false,
275 )
276
277 // Call the original method if this is the last BEFORE hook. If not, the original method will be
278 // called by the next AFTER hook.
279 if (index == matchingHooks.lastIndex) {
280 // Stack layout: ...
281 // Push the values for the original method call onto the stack again
282 if (opcode != Opcodes.INVOKESTATIC) {
283 mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
284 }
285 loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
286 // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
287 mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
288 }
289 }
290
291 HookType.REPLACE -> {
292 // Call the hook method
293 mv.visitMethodInsn(
294 Opcodes.INVOKESTATIC,
295 hook.hookInternalClassName,
296 hook.hookMethodName,
297 hook.hookMethodDescriptor,
298 false,
299 )
300 // Stack layout: ... | [return value (primitive/objectref)]
301 // Check if we need to process the return value
302 if (returnTypeDescriptor != "V") {
303 val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor)
304 // if the hook method's return type is primitive we don't need to unwrap or cast it
305 if (!isPrimitiveType(hookMethodReturnType)) {
306 // Check if the returned object type is different than the one that should be returned
307 // If a primitive should be returned we check it's wrapper type
308 val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor)
309 if (expectedType != hookMethodReturnType) {
310 // Cast object
311 mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType))
312 }
313 // Check if we need to unwrap the returned object
314 unwrapTypeIfPrimitive(returnTypeDescriptor)
315 }
316 }
317 }
318
319 HookType.AFTER -> {
320 // Call the original method before the first AFTER hook
321 if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) {
322 // Push the values for the original method call again onto the stack
323 if (opcode != Opcodes.INVOKESTATIC) {
324 mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
325 }
326 loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
327 // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
328 // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
329 mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
330 if (returnTypeDescriptor == "V") {
331 // If the method didn't return anything, we push a nullref as placeholder
332 mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
333 }
334 // Wrap return value if it is a primitive type
335 wrapTypeIfPrimitive(returnTypeDescriptor)
336 mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref
337 }
338 mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
339
340 // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
341 // | return value (objectref)
342 // Store the result value in a local variable (but keep it on the stack)
343 // Call the hook method
344 mv.visitMethodInsn(
345 Opcodes.INVOKESTATIC,
346 hook.hookInternalClassName,
347 hook.hookMethodName,
348 hook.hookMethodDescriptor,
349 false,
350 )
351 // Stack layout: ...
352 // Push the return value on the stack after the last AFTER hook if the original method returns a value
353 if (index == matchingHooks.size - 1 && returnTypeDescriptor != "V") {
354 // Push the return value again
355 mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
356 // Unwrap it, if it was a primitive value
357 unwrapTypeIfPrimitive(returnTypeDescriptor)
358 // Stack layout: ... | return value (primitive/objectref)
359 }
360 }
361 }
362 }
363 if (useConditionalHooks) {
364 // Needs a stack map frame as the target of a jump.
365 mv.visitLabel(skipHooksLabel)
366 if (postCallFrame != null) {
367 mv.visitFrame(
368 Opcodes.F_NEW,
369 postCallFrame.first?.size ?: 0,
370 postCallFrame.first,
371 postCallFrame.second?.size ?: 0,
372 postCallFrame.second,
373 )
374 }
375 // We do not control the next visitor calls, but we must not emit two frames for the
376 // same instruction.
377 mv.visitInsn(Opcodes.NOP)
378 }
379 }
380
isMethodInvocationOpnull381 private fun isMethodInvocationOp(opcode: Int) = opcode in listOf(
382 Opcodes.INVOKEVIRTUAL,
383 Opcodes.INVOKEINTERFACE,
384 Opcodes.INVOKESTATIC,
385 Opcodes.INVOKESPECIAL,
386 )
387
388 private fun findMatchingHooks(owner: String, name: String, descriptor: String): List<Hook> {
389 val result = HookType.values().flatMap { hookType ->
390 val withoutDescriptorKey = "$hookType#$owner#$name"
391 val withDescriptorKey = "$withoutDescriptorKey#$descriptor"
392 hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty()
393 }.sortedBy { it.hookType }
394 val replaceHookCount = result.count { it.hookType == HookType.REPLACE }
395 check(
396 replaceHookCount == 0 ||
397 (replaceHookCount == 1 && result.size == 1),
398 ) {
399 "For a given method, You can either have a single REPLACE hook or BEFORE/AFTER hooks. Found:\n $result"
400 }
401
402 return result
403 .filter { !isReplaceHookInJava6mode(it) }
404 .sortedByDescending { it.toString() }
405 }
406
isReplaceHookInJava6modenull407 private fun isReplaceHookInJava6mode(hook: Hook): Boolean {
408 if (java6Mode && hook.hookType == HookType.REPLACE) {
409 if (showUnsupportedHookWarning.getAndSet(false)) {
410 println(
411 """WARN: Some hooks could not be applied to class files built for Java 7 or lower.
412 |WARN: Ensure that the fuzz target and its dependencies are compiled with
413 |WARN: -target 8 or higher to identify as many bugs as possible.
414 """.trimMargin(),
415 )
416 }
417 return true
418 }
419 return false
420 }
421
422 // Stores all arguments for a method call in a local object array.
423 // paramDescriptors: The type descriptors for all method arguments
storeMethodArgumentsnull424 private fun storeMethodArguments(paramDescriptors: List<String>): Int {
425 // Allocate a new Object[] for the methods parameters.
426 mv.visitIntInsn(Opcodes.SIPUSH, paramDescriptors.size)
427 mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
428 val localObjArr = lvs.newLocal(Type.getType("[Ljava/lang/Object;"))
429 mv.visitVarInsn(Opcodes.ASTORE, localObjArr)
430
431 // Loop over all arguments in reverse order (because the last argument is on top).
432 for ((argIdx, argDescriptor) in paramDescriptors.withIndex().reversed()) {
433 // If the argument is a primitive type, wrap it in it's wrapper class
434 wrapTypeIfPrimitive(argDescriptor)
435 // Store the argument in our object array, for that we need to shape the stack first.
436 // Stack layout: ... | method argument (objectref)
437 mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
438 // Stack layout: ... | method argument (objectref) | object array (arrayref)
439 mv.visitInsn(Opcodes.SWAP)
440 // Stack layout: ... | object array (arrayref) | method argument (objectref)
441 mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
442 // Stack layout: ... | object array (arrayref) | method argument (objectref) | argument index (int)
443 mv.visitInsn(Opcodes.SWAP)
444 // Stack layout: ... | object array (arrayref) | argument index (int) | method argument (objectref)
445 mv.visitInsn(Opcodes.AASTORE) // consume all three: arrayref, index, value
446 // Stack layout: ...
447 // Continue with the remaining method arguments
448 }
449
450 // Return a reference to the array with the parameters.
451 return localObjArr
452 }
453
454 // Loads all arguments for a method call from a local object array.
455 // argTypeSigs: The type signatures for all method arguments
456 // localObjArr: Index of a local variable containing an object array where the arguments will be loaded from
loadMethodArgumentsnull457 private fun loadMethodArguments(paramDescriptors: List<String>, localObjArr: Int) {
458 // Loop over all arguments
459 for ((argIdx, argDescriptor) in paramDescriptors.withIndex()) {
460 // Push a reference to the object array on the stack
461 mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
462 // Stack layout: ... | object array (arrayref)
463 // Push the index of the current argument on the stack
464 mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
465 // Stack layout: ... | object array (arrayref) | argument index (int)
466 // Load the argument from the array
467 mv.visitInsn(Opcodes.AALOAD)
468 // Stack layout: ... | method argument (objectref)
469 // Cast object to it's original type (or it's wrapper object)
470 val wrapperTypeDescriptor = getWrapperTypeDescriptor(argDescriptor)
471 mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(wrapperTypeDescriptor))
472 // If the argument is a supposed to be a primitive type, unwrap the wrapped type
473 unwrapTypeIfPrimitive(argDescriptor)
474 // Stack layout: ... | method argument (primitive/objectref)
475 // Continue with the remaining method arguments
476 }
477 }
478
479 // Removes a primitive value from the top of the operand stack
480 // and pushes it enclosed in its wrapper type (e.g. removes int, pushes Integer).
481 // This is done by calling .valueOf(...) on the wrapper class.
wrapTypeIfPrimitivenull482 private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) {
483 if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return
484 val wrapperTypeDescriptor = getWrapperTypeDescriptor(unwrappedTypeDescriptor)
485 val wrapperType = extractInternalClassName(wrapperTypeDescriptor)
486 val valueOfDescriptor = "($unwrappedTypeDescriptor)$wrapperTypeDescriptor"
487 mv.visitMethodInsn(Opcodes.INVOKESTATIC, wrapperType, "valueOf", valueOfDescriptor, false)
488 }
489
490 // Removes a wrapper object around a given primitive type from the top of the operand stack
491 // and pushes the primitive value it contains (e.g. removes Integer, pushes int).
492 // This is done by calling .intValue(...) / .charValue(...) / ... on the wrapper object.
unwrapTypeIfPrimitivenull493 private fun unwrapTypeIfPrimitive(primitiveTypeDescriptor: String) {
494 val (methodName, wrappedTypeDescriptor) = when (primitiveTypeDescriptor) {
495 "B" -> Pair("byteValue", "java/lang/Byte")
496 "C" -> Pair("charValue", "java/lang/Character")
497 "D" -> Pair("doubleValue", "java/lang/Double")
498 "F" -> Pair("floatValue", "java/lang/Float")
499 "I" -> Pair("intValue", "java/lang/Integer")
500 "J" -> Pair("longValue", "java/lang/Long")
501 "S" -> Pair("shortValue", "java/lang/Short")
502 "Z" -> Pair("booleanValue", "java/lang/Boolean")
503 else -> return
504 }
505 mv.visitMethodInsn(
506 Opcodes.INVOKEVIRTUAL,
507 wrappedTypeDescriptor,
508 methodName,
509 "()$primitiveTypeDescriptor",
510 false,
511 )
512 }
513 }
514