<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.agent
16
17 import com.code_intelligence.jazzer.utils.Log
18 import java.nio.ByteBuffer
19 import java.nio.channels.FileChannel
20 import java.nio.channels.FileLock
21 import java.nio.file.Path
22 import java.nio.file.StandardOpenOption
23 import java.util.UUID
24
25 /**
26 * Indicates a fatal failure to generate synchronized coverage IDs.
27 */
28 class CoverageIdException(cause: Throwable? = null) :
29 RuntimeException("Failed to synchronize coverage IDs", cause)
30
31 /**
32 * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation.
33 *
34 * Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp
35 * instructions, in that they should be consecutive, collision-free, and lie in a known, small range.
36 * This precludes us from generating them simply as hashes of class names.
37 */
38 interface CoverageIdStrategy {
39
40 /**
41 * [withIdForClass] provides the initial coverage ID of the given [className] as parameter to the
42 * [block] to execute. [block] has to return the number of additionally used IDs.
43 */
44 @Throws(CoverageIdException::class)
45 fun withIdForClass(className: String, block: (Int) -> Int)
46 }
47
48 /**
49 * A memory synced strategy for coverage ID generation.
50 *
51 * This strategy uses a synchronized block to guard access to a global edge ID counter.
52 * Even though concurrent fuzzing is not fully supported this strategy enables consistent coverage
53 * IDs in case of concurrent class loading.
54 *
55 * It only prevents races within one VM instance.
56 */
57 class MemSyncCoverageIdStrategy : CoverageIdStrategy {
58 private var nextEdgeId = 0
59
60 @Synchronized
withIdForClassnull61 override fun withIdForClass(className: String, block: (Int) -> Int) {
62 nextEdgeId += block(nextEdgeId)
63 }
64 }
65
66 /**
67 * A strategy for coverage ID generation that synchronizes the IDs assigned to a class with other processes via the
68 * specified [idSyncFile].
69 * This class takes care of synchronizing the access to the file between multiple processes as long as the general
70 * contract of [CoverageIdStrategy] is followed.
71 */
72 class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy {
73 private val uuid: UUID = UUID.randomUUID()
74 private var idFileLock: FileLock? = null
75
76 private var cachedFirstId: Int? = null
77 private var cachedClassName: String? = null
78 private var cachedIdCount: Int? = null
79
80 /**
81 * This method is synchronized to prevent concurrent access to the internal file lock which would result in
82 * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId]
83 * is always committed back again to the sync file by [commitIdCount].
84 */
85 @Synchronized
withIdForClassnull86 override fun withIdForClass(className: String, block: (Int) -> Int) {
87 var actualNumEdgeIds = 0
88 try {
89 val firstId = obtainFirstId(className)
90 actualNumEdgeIds = block(firstId)
91 } finally {
92 commitIdCount(actualNumEdgeIds)
93 }
94 }
95
96 /**
97 * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID.
98 * There are two cases to consider:
99 * - This agent process is the first to encounter [className], i.e., it does not find a record for that class in
100 * [idSyncFile]. In this case, a lock on the file is held until the class has been instrumented and a record with
101 * the required number of coverage IDs has been added.
102 * - Another agent process has already encountered [className], i.e., there is a record that class in [idSyncFile].
103 * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to
104 * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected.
105 */
obtainFirstIdnull106 private fun obtainFirstId(className: String): Int {
107 try {
108 check(idFileLock == null) { "Already holding a lock on the ID file" }
109 val localIdFile = FileChannel.open(
110 idSyncFile,
111 StandardOpenOption.WRITE,
112 StandardOpenOption.READ,
113 )
114 // Wait until we have obtained the lock on the sync file. We hold the lock from this point until we have
115 // finished reading and writing (if necessary) to the file.
116 val localIdFileLock = localIdFile.lock()
117 check(localIdFileLock.isValid && !localIdFileLock.isShared)
118 // Parse the sync file, which consists of lines of the form
119 // <class name>:<first ID>:<num IDs>
120 val idInfo = localIdFileLock.channel().readFully()
121 .lineSequence()
122 .filterNot { it.isBlank() }
123 .map { line ->
124 val parts = line.split(':')
125 check(parts.size == 4) {
126 "Expected ID file line to be of the form '<class name>:<first ID>:<num IDs>:<uuid>', got '$line'"
127 }
128 val lineClassName = parts[0]
129 val lineFirstId = parts[1].toInt()
130 check(lineFirstId >= 0) { "Negative first ID in line: $line" }
131 val lineIdCount = parts[2].toInt()
132 check(lineIdCount >= 0) { "Negative ID count in line: $line" }
133 Triple(lineClassName, lineFirstId, lineIdCount)
134 }.toList()
135 cachedClassName = className
136 val idInfoForClass = idInfo.filter { it.first == className }
137 return when (idInfoForClass.size) {
138 0 -> {
139 // We are the first to encounter this class and thus need to hold the lock until the class has been
140 // instrumented and we know the required number of coverage IDs.
141 idFileLock = localIdFileLock
142 // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if
143 // this is the first ID to be assigned. In fact, since this is the only way new lines are added to
144 // the file, the maximum is always attained by the last line.
145 val nextFreeId = idInfo.asSequence().map { it.second + it.third }.lastOrNull() ?: 0
146 cachedFirstId = nextFreeId
147 nextFreeId
148 }
149 1 -> {
150 // This class has already been instrumented elsewhere, so we just return the first ID and ID count
151 // reported from there and release the lock right away. The caller is still expected to call
152 // commitIdCount.
153 localIdFile.close()
154 cachedIdCount = idInfoForClass.single().third
155 idInfoForClass.single().second
156 }
157 else -> {
158 localIdFile.close()
159 Log.println(idInfo.joinToString("\n") { "${it.first}:${it.second}:${it.third}" })
160 throw IllegalStateException("Multiple entries for $className in ID file")
161 }
162 }
163 } catch (e: Exception) {
164 throw CoverageIdException(e)
165 }
166 }
167
168 /**
169 * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId].
170 * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0.
171 */
commitIdCountnull172 private fun commitIdCount(idCount: Int) {
173 val localIdFileLock = idFileLock
174 try {
175 check(cachedClassName != null)
176 if (localIdFileLock == null) {
177 // We released the lock already in obtainFirstId since the class had already been instrumented
178 // elsewhere. As we know the expected number of IDs for the current class in this case, check for
179 // deviations.
180 check(cachedIdCount != null)
181 check(idCount == cachedIdCount) {
182 "$cachedClassName has $idCount edges, but $cachedIdCount edges reserved in ID file"
183 }
184 } else {
185 // We are the first to instrument this class and should record the number of IDs in the sync file.
186 check(cachedFirstId != null)
187 localIdFileLock.channel().append("$cachedClassName:$cachedFirstId:$idCount:$uuid\n")
188 localIdFileLock.channel().force(true)
189 }
190 idFileLock = null
191 cachedFirstId = null
192 cachedIdCount = null
193 cachedClassName = null
194 } catch (e: Exception) {
195 throw CoverageIdException(e)
196 } finally {
197 localIdFileLock?.channel()?.close()
198 }
199 }
200 }
201
202 /**
203 * Reads the [FileChannel] to the end as a UTF-8 string.
204 */
FileChannelnull205 fun FileChannel.readFully(): String {
206 check(size() <= Int.MAX_VALUE)
207 val buffer = ByteBuffer.allocate(size().toInt())
208 while (buffer.hasRemaining()) {
209 when (read(buffer)) {
210 0 -> throw IllegalStateException("No bytes read")
211 -1 -> break
212 }
213 }
214 return String(buffer.array())
215 }
216
217 /**
218 * Appends [string] to the end of the [FileChannel].
219 */
FileChannelnull220 fun FileChannel.append(string: String) {
221 position(size())
222 write(ByteBuffer.wrap(string.toByteArray()))
223 }
224