<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.runtime.CoverageMap
18 import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.CoverageBuilder
19 import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData
20 import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore
21 import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataWriter
22 import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfo
23 import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.data.CRC64
24 import com.code_intelligence.jazzer.utils.ClassNameGlobber
25 import io.github.classgraph.ClassGraph
26 import java.io.File
27 import java.io.FileOutputStream
28 import java.io.OutputStream
29 import java.time.Instant
30 import java.util.UUID
31
32 private data class InstrumentedClassInfo(
33 val classId: Long,
34 val initialEdgeId: Int,
35 val nextEdgeId: Int,
36 val bytecode: ByteArray,
37 )
38
39 object CoverageRecorder {
40 var classNameGlobber = ClassNameGlobber(emptyList(), emptyList())
41 private val instrumentedClassInfo = mutableMapOf<String, InstrumentedClassInfo>()
42 private var startTimestamp: Instant? = null
43 private val additionalCoverage = mutableSetOf<Int>()
44
45 fun recordInstrumentedClass(internalClassName: String, bytecode: ByteArray, firstId: Int, numIds: Int) {
46 if (startTimestamp == null) {
47 startTimestamp = Instant.now()
48 }
49 instrumentedClassInfo[internalClassName] = InstrumentedClassInfo(
50 CRC64.classId(bytecode),
51 firstId,
52 firstId + numIds,
53 bytecode,
54 )
55 }
56
57 /**
58 * Manually records coverage IDs based on the current state of [CoverageMap].
59 * Should be called after static initializers have run.
60 */
61 @JvmStatic
62 fun updateCoveredIdsWithCoverageMap() {
63 additionalCoverage.addAll(CoverageMap.getCoveredIds())
64 }
65
66 /**
67 * [dumpCoverageReport] dumps a human-readable coverage report of files using any [coveredIds] to [dumpFileName].
68 */
69 @JvmStatic
70 @JvmOverloads
71 fun dumpCoverageReport(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) {
72 File(dumpFileName).bufferedWriter().use { writer ->
73 writer.write(computeFileCoverage(coveredIds))
74 }
75 }
76
77 private fun computeFileCoverage(coveredIds: IntArray): String {
78 fun Double.format(digits: Int) = "%.${digits}f".format(this)
79 val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented"
80 return coverage.sourceFiles.joinToString(
81 "\n",
82 prefix = "Branch coverage:\n",
83 postfix = "\n\n",
84 ) { fileCoverage ->
85 val counter = fileCoverage.branchCounter
86 val percentage = 100 * counter.coveredRatio
87 "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
88 } + coverage.sourceFiles.joinToString(
89 "\n",
90 prefix = "Line coverage:\n",
91 postfix = "\n\n",
92 ) { fileCoverage ->
93 val counter = fileCoverage.lineCounter
94 val percentage = 100 * counter.coveredRatio
95 "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
96 } + coverage.sourceFiles.joinToString(
97 "\n",
98 prefix = "Incompletely covered lines:\n",
99 postfix = "\n\n",
100 ) { fileCoverage ->
101 "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
102 val instructions = fileCoverage.getLine(it).instructionCounter
103 instructions.coveredCount in 1 until instructions.totalCount
104 }.toString()
105 } + coverage.sourceFiles.joinToString(
106 "\n",
107 prefix = "Missed lines:\n",
108 ) { fileCoverage ->
109 "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
110 val instructions = fileCoverage.getLine(it).instructionCounter
111 instructions.coveredCount == 0 && instructions.totalCount > 0
112 }.toString()
113 }
114 }
115
116 /**
117 * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [dumpFileName].
118 * JaCoCo only exports coverage for files containing at least one coverage data point. The dump
119 * can be used by the JaCoCo report command to create reports also including not covered files.
120 */
121 @JvmStatic
122 @JvmOverloads
123 fun dumpJacocoCoverage(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) {
124 FileOutputStream(dumpFileName).use { outStream ->
125 dumpJacocoCoverage(outStream, coveredIds)
126 }
127 }
128
129 /**
130 * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream].
131 */
132 @JvmStatic
133 fun dumpJacocoCoverage(outStream: OutputStream, coveredIds: IntArray) {
134 // Return if no class has been instrumented.
135 val startTimestamp = startTimestamp ?: return
136
137 // Update the list of covered IDs with the coverage information for the current run.
138 updateCoveredIdsWithCoverageMap()
139
140 val dumpTimestamp = Instant.now()
141 val outWriter = ExecutionDataWriter(outStream)
142 outWriter.visitSessionInfo(
143 SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond),
144 )
145 analyzeJacocoCoverage(coveredIds.toSet()).accept(outWriter)
146 }
147
148 /**
149 * Build up a JaCoCo [ExecutionDataStore] based on [coveredIds] containing the internally gathered coverage information.
150 */
151 private fun analyzeJacocoCoverage(coveredIds: Set<Int>): ExecutionDataStore {
152 val executionDataStore = ExecutionDataStore()
153 val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray()
154 for ((internalClassName, info) in instrumentedClassInfo) {
155 // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while
156 // instrumenting the current class. Since the ID array is sorted, use binary search.
157 var coveredIdsStart = sortedCoveredIds.binarySearch(info.initialEdgeId)
158 if (coveredIdsStart < 0) {
159 coveredIdsStart = -(coveredIdsStart + 1)
160 }
161 var coveredIdsEnd = sortedCoveredIds.binarySearch(info.nextEdgeId)
162 if (coveredIdsEnd < 0) {
163 coveredIdsEnd = -(coveredIdsEnd + 1)
164 }
165 if (coveredIdsStart == coveredIdsEnd) {
166 // No coverage data for the class.
167 continue
168 }
169 check(coveredIdsStart in 0 until coveredIdsEnd && coveredIdsEnd <= sortedCoveredIds.size) {
170 "Invalid range [$coveredIdsStart, $coveredIdsEnd) with coveredIds.size=${sortedCoveredIds.size}"
171 }
172 // Generate a probes array for the current class only, i.e., mapping info.initialEdgeId to 0.
173 val probes = BooleanArray(info.nextEdgeId - info.initialEdgeId)
174 (coveredIdsStart until coveredIdsEnd).asSequence()
175 .map {
176 val globalEdgeId = sortedCoveredIds[it]
177 globalEdgeId - info.initialEdgeId
178 }
179 .forEach { classLocalEdgeId ->
180 probes[classLocalEdgeId] = true
181 }
182 executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes))
183 }
184 return executionDataStore
185 }
186
187 /**
188 * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics.
189 */
190 fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? {
191 return try {
192 val coverage = CoverageBuilder()
193 analyzeAllUncoveredClasses(coverage)
194 val executionDataStore = analyzeJacocoCoverage(coveredIds)
195 for ((internalClassName, info) in instrumentedClassInfo) {
196 EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0)
197 .analyze(
198 executionDataStore,
199 coverage,
200 info.bytecode,
201 internalClassName,
202 )
203 }
204 coverage
205 } catch (e: Exception) {
206 e.printStackTrace()
207 null
208 }
209 }
210
211 /**
212 * Traverses the entire classpath and analyzes all uncovered classes that match the include/exclude pattern.
213 * The returned [CoverageBuilder] will report coverage information for *all* classes on the classpath, not just
214 * those that were loaded while the fuzzer ran.
215 */
216 private fun analyzeAllUncoveredClasses(coverage: CoverageBuilder): CoverageBuilder {
217 val coveredClassNames = instrumentedClassInfo
218 .keys
219 .asSequence()
220 .map { it.replace('/', '.') }
221 .toSet()
222 ClassGraph()
223 .enableClassInfo()
224 .ignoreClassVisibility()
225 .rejectPackages(
226 // Always exclude Jazzer-internal packages (including ClassGraph itself) from coverage reports. Classes
227 // from the Java standard library are never traversed.
228 "com.code_intelligence.jazzer.*",
229 "jaz",
230 )
231 .scan().use { result ->
232 // ExecutionDataStore is used to look up existing coverage during analysis of the class files,
233 // no entries are added during that. Passing in an empty store is fine for uncovered files.
234 val emptyExecutionDataStore = ExecutionDataStore()
235 result.allClasses
236 .asSequence()
237 .filter { classInfo -> classNameGlobber.includes(classInfo.name) }
238 .filterNot { classInfo -> classInfo.name in coveredClassNames }
239 .forEach { classInfo ->
240 classInfo.resource.use { resource ->
241 EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze(
242 emptyExecutionDataStore,
243 coverage,
244 resource.load(),
245 classInfo.name.replace('.', '/'),
246 )
247 }
248 }
249 }
250 return coverage
251 }
252 }
253