aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt
blob: 9bcd744f550d54a66cd0c69651f1593edc23839f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Copyright 2021 Code Intelligence GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

@file:JvmName("Agent")

package com.code_intelligence.jazzer.agent

import com.code_intelligence.jazzer.driver.Opt
import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
import com.code_intelligence.jazzer.instrumentor.Hooks
import com.code_intelligence.jazzer.instrumentor.InstrumentationType
import com.code_intelligence.jazzer.sanitizers.Constants
import com.code_intelligence.jazzer.utils.ClassNameGlobber
import com.code_intelligence.jazzer.utils.Log
import com.code_intelligence.jazzer.utils.ManifestUtils
import java.lang.instrument.Instrumentation
import java.nio.file.Paths
import kotlin.io.path.exists
import kotlin.io.path.isDirectory

fun install(instrumentation: Instrumentation) {
    installInternal(instrumentation)
}

fun installInternal(
    instrumentation: Instrumentation,
    userHookNames: List<String> = findManifestCustomHookNames() + Opt.customHooks,
    disabledHookNames: List<String> = Opt.disabledHooks,
    instrumentationIncludes: List<String> = Opt.instrumentationIncludes.get(),
    instrumentationExcludes: List<String> = Opt.instrumentationExcludes.get(),
    customHookIncludes: List<String> = Opt.customHookIncludes.get(),
    customHookExcludes: List<String> = Opt.customHookExcludes.get(),
    trace: List<String> = Opt.trace,
    idSyncFile: String? = Opt.idSyncFile,
    dumpClassesDir: String = Opt.dumpClassesDir,
    additionalClassesExcludes: List<String> = Opt.additionalClassesExcludes,
) {
    val allCustomHookNames = (Constants.SANITIZER_HOOK_NAMES + userHookNames).toSet()
    check(allCustomHookNames.isNotEmpty()) { "No hooks registered; expected at least the built-in hooks" }
    val customHookNames = allCustomHookNames - disabledHookNames.toSet()
    val disabledCustomHooksToPrint = allCustomHookNames - customHookNames.toSet()
    if (disabledCustomHooksToPrint.isNotEmpty()) {
        Log.info("Not using the following disabled hooks: ${disabledCustomHooksToPrint.joinToString(", ")}")
    }

    val classNameGlobber = ClassNameGlobber(instrumentationIncludes, instrumentationExcludes + customHookNames)
    CoverageRecorder.classNameGlobber = classNameGlobber
    val customHookClassNameGlobber = ClassNameGlobber(customHookIncludes, customHookExcludes + customHookNames)
    // FIXME: Setting trace to the empty string explicitly results in all rather than no trace types
    //  being applied - this is unintuitive.
    val instrumentationTypes = (trace.takeIf { it.isNotEmpty() } ?: listOf("all")).flatMap {
        when (it) {
            "cmp" -> setOf(InstrumentationType.CMP)
            "cov" -> setOf(InstrumentationType.COV)
            "div" -> setOf(InstrumentationType.DIV)
            "gep" -> setOf(InstrumentationType.GEP)
            "indir" -> setOf(InstrumentationType.INDIR)
            "native" -> setOf(InstrumentationType.NATIVE)
            // Disable GEP instrumentation by default as it appears to negatively affect fuzzing
            // performance. Our current GEP instrumentation only reports constant indices, but even
            // when we instead reported non-constant indices, they tended to completely fill up the
            // table of recent compares and value profile map.
            "all" -> InstrumentationType.values().toSet() - InstrumentationType.GEP
            else -> {
                println("WARN: Skipping unknown instrumentation type $it")
                emptySet()
            }
        }
    }.toSet()

    val idSyncFilePath = idSyncFile?.takeUnless { it.isEmpty() }?.let {
        Paths.get(it).also { path ->
            Log.info("Synchronizing coverage IDs in ${path.toAbsolutePath()}")
        }
    }
    val dumpClassesDirPath = dumpClassesDir.takeUnless { it.isEmpty() }?.let {
        Paths.get(it).toAbsolutePath().also { path ->
            if (path.exists() && path.isDirectory()) {
                Log.info("Dumping instrumented classes into $path")
            } else {
                Log.error("Cannot dump instrumented classes into $path; does not exist or not a directory")
            }
        }
    }
    val includedHookNames = instrumentationTypes
        .mapNotNull { type ->
            when (type) {
                InstrumentationType.CMP -> "com.code_intelligence.jazzer.runtime.TraceCmpHooks"
                InstrumentationType.DIV -> "com.code_intelligence.jazzer.runtime.TraceDivHooks"
                InstrumentationType.INDIR -> "com.code_intelligence.jazzer.runtime.TraceIndirHooks"
                InstrumentationType.NATIVE -> "com.code_intelligence.jazzer.runtime.NativeLibHooks"
                else -> null
            }
        }
    val coverageIdSynchronizer = if (idSyncFilePath != null) {
        FileSyncCoverageIdStrategy(idSyncFilePath)
    } else {
        MemSyncCoverageIdStrategy()
    }

    // If we don't append the JARs containing the custom hooks to the bootstrap class loader,
    // third-party hooks not contained in the agent JAR will not be able to instrument Java standard
    // library classes. These classes are loaded by the bootstrap / system class loader and would
    // not be considered when resolving references to hook methods, leading to NoClassDefFoundError
    // being thrown.
    Hooks.appendHooksToBootstrapClassLoaderSearch(instrumentation, customHookNames.toSet())
    val (includedHooks, customHooks) = Hooks.loadHooks(additionalClassesExcludes, includedHookNames.toSet(), customHookNames.toSet())

    val runtimeInstrumentor = RuntimeInstrumentor(
        instrumentation,
        classNameGlobber,
        customHookClassNameGlobber,
        instrumentationTypes,
        includedHooks.hooks,
        customHooks.hooks,
        customHooks.additionalHookClassNameGlobber,
        coverageIdSynchronizer,
        dumpClassesDirPath,
    )

    // These classes are e.g. dependencies of the RuntimeInstrumentor or hooks and thus were loaded
    // before the instrumentor was ready. Since we haven't enabled it yet, they can safely be
    // "retransformed": They haven't been transformed yet.
    val classesToRetransform = instrumentation.allLoadedClasses
        .filter {
            classNameGlobber.includes(it.name) ||
                customHookClassNameGlobber.includes(it.name) ||
                customHooks.additionalHookClassNameGlobber.includes(it.name)
        }
        .filter {
            instrumentation.isModifiableClass(it)
        }
        .toTypedArray()

    instrumentation.addTransformer(runtimeInstrumentor, true)

    if (classesToRetransform.isNotEmpty()) {
        if (instrumentation.isRetransformClassesSupported) {
            retransformClassesWithRetry(instrumentation, classesToRetransform)
        }
    }
}

private fun retransformClassesWithRetry(instrumentation: Instrumentation, classesToRetransform: Array<Class<*>>) {
    try {
        instrumentation.retransformClasses(*classesToRetransform)
    } catch (e: Throwable) {
        if (classesToRetransform.size == 1) {
            Log.warn("Error retransforming class ${classesToRetransform[0].name }", e)
        } else {
            // The docs state that no transformation was performed if an exception is thrown.
            // Try again in a binary search fashion, until the not transformable classes have been isolated and reported.
            retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(0, classesToRetransform.size / 2))
            retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(classesToRetransform.size / 2, classesToRetransform.size))
        }
    }
}

private fun findManifestCustomHookNames() = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES)
    .flatMap { it.split(':') }
    .filter { it.isNotBlank() }