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