Package FuzzManager :: Package FTB :: Package Signatures :: Module Symptom
[hide private]
[frames] | no frames]

Source Code for Module FuzzManager.FTB.Signatures.Symptom

  1  ''' 
  2  Symptom 
  3   
  4  Represents one symptom which may appear in a crash signature. 
  5   
  6  @author:     Christian Holler (:decoder) 
  7   
  8  @license: 
  9   
 10  This Source Code Form is subject to the terms of the Mozilla Public 
 11  License, v. 2.0. If a copy of the MPL was not distributed with this 
 12  file, You can obtain one at http://mozilla.org/MPL/2.0/. 
 13   
 14  @contact:    choller@mozilla.com 
 15  ''' 
 16   
 17  # Ensure print() compatibility with Python 3 
 18  from __future__ import print_function 
 19   
 20  from abc import ABCMeta, abstractmethod 
 21  import json 
 22  from FTB.Signatures import JSONHelper 
 23  from FTB.Signatures.Matchers import StringMatch, NumberMatch 
24 25 -class Symptom():
26 ''' 27 Abstract base class that provides a method to instantiate the right sub class. 28 It also supports generating a CrashSignature based on the stored information. 29 ''' 30 __metaclass__ = ABCMeta 31
32 - def __init__(self, jsonObj):
33 # Store the original source so we can return it if someone wants to stringify us 34 self.jsonsrc = json.dumps(jsonObj, indent=2) 35 self.jsonobj = jsonObj
36
37 - def __str__(self):
38 return self.jsonsrc
39 40 @staticmethod
41 - def fromJSONObject(obj):
42 ''' 43 Create the appropriate Symptom based on the given object (decoded from JSON) 44 45 @type obj: map 46 @param obj: Object as decoded from JSON 47 48 @rtype: Symptom 49 @return: Symptom subclass instance matching the given object 50 ''' 51 if not "type" in obj: 52 raise RuntimeError("Missing symptom type in object") 53 54 stype = obj["type"] 55 56 if (stype == "output"): 57 return OutputSymptom(obj) 58 elif (stype == "stackFrame"): 59 return StackFrameSymptom(obj) 60 elif (stype == "stackSize"): 61 return StackSizeSymptom(obj) 62 elif (stype == "crashAddress"): 63 return CrashAddressSymptom(obj) 64 elif (stype == "instruction"): 65 return InstructionSymptom(obj) 66 elif (stype == "testcase"): 67 return TestcaseSymptom(obj) 68 elif (stype == "stackFrames"): 69 return StackFramesSymptom(obj) 70 else: 71 raise RuntimeError("Unknown symptom type: %s" % stype)
72 73 @abstractmethod
74 - def matches(self, crashInfo):
75 ''' 76 Check if the symptom matches the given crash information 77 78 @type crashInfo: CrashInfo 79 @param crashInfo: The crash information to check against 80 81 @rtype: bool 82 @return: True if the symptom matches, False otherwise 83 ''' 84 return
85
86 87 -class OutputSymptom(Symptom):
88 - def __init__(self, obj):
89 ''' 90 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 91 ''' 92 Symptom.__init__(self, obj) 93 self.output = StringMatch(JSONHelper.getObjectOrStringChecked(obj, "value", True)) 94 self.src = JSONHelper.getStringChecked(obj, "src") 95 96 if self.src != None: 97 self.src = self.src.lower() 98 if self.src != "stderr" and self.src != "stdout" and self.src != "crashdata": 99 raise RuntimeError("Invalid source specified: %s" % self.src)
100
101 - def matches(self, crashInfo):
102 ''' 103 Check if the symptom matches the given crash information 104 105 @type crashInfo: CrashInfo 106 @param crashInfo: The crash information to check against 107 108 @rtype: bool 109 @return: True if the symptom matches, False otherwise 110 ''' 111 checkedOutput = [] 112 113 if self.src == None: 114 checkedOutput.extend(crashInfo.rawStdout) 115 checkedOutput.extend(crashInfo.rawStderr) 116 checkedOutput.extend(crashInfo.rawCrashData) 117 elif (self.src == "stdout"): 118 checkedOutput = crashInfo.rawStdout 119 elif (self.src == "stderr"): 120 checkedOutput = crashInfo.rawStderr 121 else: 122 checkedOutput = crashInfo.rawCrashData 123 124 for line in checkedOutput: 125 if self.output.matches(line): 126 return True 127 128 return False
129
130 -class StackFrameSymptom(Symptom):
131 - def __init__(self, obj):
132 ''' 133 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 134 ''' 135 Symptom.__init__(self, obj) 136 self.functionName = StringMatch(JSONHelper.getNumberOrStringChecked(obj, "functionName", True)) 137 self.frameNumber = JSONHelper.getNumberOrStringChecked(obj, "frameNumber") 138 139 if self.frameNumber != None: 140 self.frameNumber = NumberMatch(self.frameNumber) 141 else: 142 # Default to 0 143 self.frameNumber = NumberMatch(0)
144
145 - def matches(self, crashInfo):
146 ''' 147 Check if the symptom matches the given crash information 148 149 @type crashInfo: CrashInfo 150 @param crashInfo: The crash information to check against 151 152 @rtype: bool 153 @return: True if the symptom matches, False otherwise 154 ''' 155 156 for idx in range(len(crashInfo.backtrace)): 157 # Not the most efficient way for very long stacks with a small match area 158 if self.frameNumber.matches(idx): 159 if self.functionName.matches(crashInfo.backtrace[idx]): 160 return True 161 162 return False
163
164 -class StackSizeSymptom(Symptom):
165 - def __init__(self, obj):
166 ''' 167 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 168 ''' 169 Symptom.__init__(self, obj) 170 self.stackSize = NumberMatch(JSONHelper.getNumberOrStringChecked(obj, "size", True))
171
172 - def matches(self, crashInfo):
173 ''' 174 Check if the symptom matches the given crash information 175 176 @type crashInfo: CrashInfo 177 @param crashInfo: The crash information to check against 178 179 @rtype: bool 180 @return: True if the symptom matches, False otherwise 181 ''' 182 return self.stackSize.matches(len(crashInfo.backtrace))
183
184 -class CrashAddressSymptom(Symptom):
185 - def __init__(self, obj):
186 ''' 187 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 188 ''' 189 Symptom.__init__(self, obj) 190 self.address = NumberMatch(JSONHelper.getNumberOrStringChecked(obj, "address", True))
191
192 - def matches(self, crashInfo):
193 ''' 194 Check if the symptom matches the given crash information 195 196 @type crashInfo: CrashInfo 197 @param crashInfo: The crash information to check against 198 199 @rtype: bool 200 @return: True if the symptom matches, False otherwise 201 ''' 202 # In case the crash address is not available, 203 # the NumberMatch class will return false to not match. 204 return self.address.matches(crashInfo.crashAddress)
205
206 -class InstructionSymptom(Symptom):
207 - def __init__(self, obj):
208 ''' 209 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 210 ''' 211 Symptom.__init__(self, obj) 212 self.registerNames = JSONHelper.getArrayChecked(obj, "registerNames") 213 self.instructionName = JSONHelper.getObjectOrStringChecked(obj, "instructionName") 214 215 if self.instructionName != None: 216 self.instructionName = StringMatch(self.instructionName) 217 elif self.registerNames == None or len(self.registerNames) == 0: 218 raise RuntimeError("Must provide at least instruction name or register names")
219
220 - def matches(self, crashInfo):
221 ''' 222 Check if the symptom matches the given crash information 223 224 @type crashInfo: CrashInfo 225 @param crashInfo: The crash information to check against 226 227 @rtype: bool 228 @return: True if the symptom matches, False otherwise 229 ''' 230 if crashInfo.crashInstruction == None: 231 # No crash instruction available, do not match 232 return False 233 234 if self.registerNames != None: 235 for register in self.registerNames: 236 if not register in crashInfo.crashInstruction: 237 return False 238 239 if self.instructionName != None: 240 if not self.instructionName.matches(crashInfo.crashInstruction): 241 return False 242 243 return True
244
245 -class TestcaseSymptom(Symptom):
246 - def __init__(self, obj):
247 ''' 248 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 249 ''' 250 Symptom.__init__(self, obj) 251 self.output = StringMatch(JSONHelper.getObjectOrStringChecked(obj, "value", True))
252
253 - def matches(self, crashInfo):
254 ''' 255 Check if the symptom matches the given crash information 256 257 @type crashInfo: CrashInfo 258 @param crashInfo: The crash information to check against 259 260 @rtype: bool 261 @return: True if the symptom matches, False otherwise 262 ''' 263 264 # No testcase means to fail matching 265 if crashInfo.testcase == None: 266 return False 267 268 testLines = crashInfo.testcase.splitlines() 269 270 for line in testLines: 271 if self.output.matches(line): 272 return True 273 274 return False
275
276 -class StackFramesSymptom(Symptom):
277 - def __init__(self, obj):
278 ''' 279 Private constructor, called by L{Symptom.fromJSONObject}. Do not use directly. 280 ''' 281 Symptom.__init__(self, obj) 282 self.functionNames = [] 283 284 rawFunctionNames = JSONHelper.getArrayChecked(obj, "functionNames", True) 285 286 for fn in rawFunctionNames: 287 self.functionNames.append(StringMatch(fn))
288
289 - def matches(self, crashInfo):
290 ''' 291 Check if the symptom matches the given crash information 292 293 @type crashInfo: CrashInfo 294 @param crashInfo: The crash information to check against 295 296 @rtype: bool 297 @return: True if the symptom matches, False otherwise 298 ''' 299 300 return StackFramesSymptom._match(crashInfo.backtrace, self.functionNames)
301
302 - def diff(self, crashInfo):
303 if self.matches(crashInfo): 304 return (0, None) 305 306 for depth in range(1,4): 307 (bestDepth, bestGuess) = StackFramesSymptom._diff(crashInfo.backtrace, self.functionNames, 0, 1, depth) 308 if bestDepth != None: 309 guessedFunctionNames = [repr(x) for x in bestGuess] 310 311 # Remove trailing wildcards as they are of no use 312 while guessedFunctionNames and (guessedFunctionNames[-1] == '?' or guessedFunctionNames[-1] == '???'): 313 guessedFunctionNames.pop() 314 315 if not guessedFunctionNames: 316 # Do not return empty matches. This happens if there's nothing left except wildcards. 317 return (None, None) 318 319 return (bestDepth, StackFramesSymptom({ "type": "stackFrames", 'functionNames' : guessedFunctionNames })) 320 321 return (None, None)
322 323 @staticmethod
324 - def _diff(stack, signatureGuess, startIdx, depth, maxDepth):
325 singleWildcardMatch = StringMatch("?") 326 327 newSignatureGuess = [] 328 newSignatureGuess.extend(signatureGuess) 329 330 bestDepth = None 331 bestGuess = None 332 333 hasVariableStackLengthQuantifier = '???' in [str(x) for x in newSignatureGuess] 334 335 for idx in range(startIdx,len(newSignatureGuess)): 336 newSignatureGuess.insert(idx, singleWildcardMatch) 337 338 # Check if we have a match with our modification 339 if StackFramesSymptom._match(stack, newSignatureGuess): 340 return (depth, newSignatureGuess) 341 342 # If we don't have a match but we're not at our current depth limit, 343 # add one more level of depth for our search. 344 if depth < maxDepth: 345 (newBestDepth, newBestGuess) = StackFramesSymptom._diff(stack, newSignatureGuess, idx, depth+1, maxDepth) 346 347 if newBestDepth != None and (bestDepth == None or newBestDepth < bestDepth): 348 bestDepth = newBestDepth 349 bestGuess = newBestGuess 350 351 newSignatureGuess.pop(idx) 352 353 # Now repeat the same with replacing instead of adding 354 # unless the match at idx is a wildcard itself 355 356 if str(newSignatureGuess[idx]) == '?' or str(newSignatureGuess[idx]) == '???': 357 continue 358 359 newMatch = singleWildcardMatch 360 if not hasVariableStackLengthQuantifier and len(stack) > idx: 361 # We can perform some optimizations here if we have a signature that does 362 # not contain any quantifiers that can match multiple stack frames. 363 364 if newSignatureGuess[idx].matches(stack[idx]): 365 # Our frame matches, so it doesn't make sense to try and mess with it 366 continue 367 368 if not newSignatureGuess[idx].isPCRE: 369 # If our match is not PCRE, try some heuristics to generalize the match 370 371 if stack[idx] in str(newSignatureGuess[idx]): 372 # The stack frame is a substring of the what we try to match, 373 # use the stack frame as new matcher to ensure a match without 374 # using a wildcard. 375 newMatch = StringMatch(stack[idx]) 376 #else: 377 # def getGreatestCommonPrefix(a,b): 378 # maxlen = min(len(a),len(b)) 379 # for i in range(0,maxlen): 380 # if (a[i] != b[i]): 381 # return a[0:max(0,i-1)] 382 # return a[0:maxlen] 383 # 384 # gcp = getGreatestCommonPrefix(stack[idx], str(newSignatureGuess[idx])) 385 386 # if (len(gcp) >= 3): 387 # TODO: We could go on here now and make partial matches but we wan't to prevent 388 # namespaces from being the only common thing amongst two frames, so we don't create 389 # matches like js::(foo|bar). For that, we have to find a good way to avoid that, maybe 390 # parse the namespaces of the stack frame. 391 392 393 394 origMatch = newSignatureGuess[idx] 395 newSignatureGuess[idx] = newMatch 396 397 # Check if we have a match with our modification 398 if StackFramesSymptom._match(stack, newSignatureGuess): 399 return (depth, newSignatureGuess) 400 401 # If we don't have a match but we're not at our current depth limit, 402 # add one more level of depth for our search. 403 if depth < maxDepth: 404 (newBestDepth, newBestGuess) = StackFramesSymptom._diff(stack, newSignatureGuess, idx, depth+1, maxDepth) 405 406 if newBestDepth != None and (bestDepth == None or newBestDepth < bestDepth): 407 bestDepth = newBestDepth 408 bestGuess = newBestGuess 409 410 newSignatureGuess[idx] = origMatch 411 412 return (bestDepth, bestGuess)
413 414 @staticmethod
415 - def _match(partialStack, partialFunctionNames):
416 # Process as many non-wildcard chars as we can find iteratively for performance reasons 417 while partialFunctionNames and partialStack and str(partialFunctionNames[0]) != '?' and str(partialFunctionNames[0]) != '???': 418 if not partialFunctionNames[0].matches(partialStack[0]): 419 return False 420 421 # Change the view on partialStack and partialFunctionNames without actually 422 # modifying the underlying arrays. They have to be preserved for the caller. 423 partialStack = partialStack[1:] 424 partialFunctionNames = partialFunctionNames[1:] 425 426 if not partialFunctionNames: 427 # End of function names to match, accept 428 return True 429 430 if str(partialFunctionNames[0]) == '?' or str(partialFunctionNames[0]) == '???': 431 if StackFramesSymptom._match(partialStack, partialFunctionNames[1:]): 432 # We recursively consumed 0 to N stack frames and can now 433 # get a match for the remaining stack without the current 434 # wildcard element, so we're done and accept the stack. 435 return True 436 else: 437 if not partialStack: 438 # Out of stack to match, reject 439 return False 440 441 if str(partialFunctionNames[0]) == '?': 442 # Recurse, consume one stack frame and the question mark 443 return StackFramesSymptom._match(partialStack[1:], partialFunctionNames[1:]) 444 else: 445 # Recurse, consume one stack frame and keep triple question mark 446 return StackFramesSymptom._match(partialStack[1:], partialFunctionNames) 447 elif not partialStack: 448 # Out of stack to match, reject 449 return False 450