<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