• 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.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