macos_crash_report.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
  3. import json
  4. import posixpath
  5. import sys
  6. from collections import namedtuple
  7. from datetime import datetime
  8. from enum import Enum
  9. from functools import cached_property
  10. from typing import IO, List, Mapping, Optional
  11. Frame = namedtuple('Frame', 'image_name image_base image_offset symbol symbol_offset')
  12. Register = namedtuple('Register', 'name value')
  13. def surround(x: str, start: int, end: int) -> str:
  14. if sys.stdout.isatty():
  15. x = f'\033[{start}m{x}\033[{end}m'
  16. return x
  17. def cyan(x: str) -> str:
  18. return surround(x, 96, 39)
  19. def bold(x: str) -> str:
  20. return surround(x, 1, 22)
  21. class BugType(Enum):
  22. WatchdogTimeout = '28'
  23. BasebandStats = '195'
  24. GPUEvent = '284'
  25. Sandbox = '187'
  26. TerminatingStackshot = '509'
  27. ServiceWatchdogTimeout = '29'
  28. Session = '179'
  29. LegacyStackshot = '188'
  30. MACorrelation = '197'
  31. iMessages = '189'
  32. log_power = '278'
  33. PowerLog = 'powerlog'
  34. DuetKnowledgeCollector2 = '58'
  35. BridgeRestore = '83'
  36. LegacyJetsam = '198'
  37. ExcResource_385 = '385'
  38. Modem = '199'
  39. Stackshot = '288'
  40. SystemInformation = 'system_profile'
  41. Jetsam_298 = '298'
  42. MemoryResource = '30'
  43. Bridge = '31'
  44. DifferentialPrivacy = 'diff_privacy'
  45. FirmwareIntegrity = '32'
  46. CoreAnalytics_33 = '33'
  47. AutoBugCapture = '34'
  48. EfiFirmwareIntegrity = '35'
  49. SystemStats = '36'
  50. AnonSystemStats = '37'
  51. Crash_9 = '9'
  52. Jetsam_98 = '98'
  53. LDCM = '100'
  54. Panic_10 = '10'
  55. Spin = '11'
  56. CLTM = '101'
  57. Hang = '12'
  58. Panic_110 = '110'
  59. ConnectionFailure = '13'
  60. MessageTracer = '14'
  61. LowBattery = '120'
  62. Siri = '201'
  63. ShutdownStall = '17'
  64. Panic_210 = '210'
  65. SymptomsCPUUsage = '202'
  66. AssumptionViolation = '18'
  67. CoreHandwriting = 'chw'
  68. IOMicroStackShot = '44'
  69. CoreAnalytics_211 = '211'
  70. SiriAppPrediction = '203'
  71. spin_45 = '45'
  72. PowerMicroStackshots = '220'
  73. BTMetadata = '212'
  74. SystemMemoryReset = '301'
  75. ResetCount = '115'
  76. AutoBugCapture_204 = '204'
  77. WifiCrashBinary = '221'
  78. MicroRunloopHang = '310'
  79. Rosetta = '213'
  80. glitchyspin = '302'
  81. System = '116'
  82. IOPowerSources = '141'
  83. PanicStats = '205'
  84. PowerLog_230 = '230'
  85. LongRunloopHang = '222'
  86. HomeProductsAnalytics = '311'
  87. DifferentialPrivacy_150 = '150'
  88. Rhodes = '214'
  89. ProactiveEventTrackerTransparency = '303'
  90. WiFi = '117'
  91. SymptomsCPUWakes = '142'
  92. SymptomsCPUUsageFatal = '206'
  93. Crash_109 = '109'
  94. ShortRunloopHang = '223'
  95. CoreHandwriting_231 = '231'
  96. ForceReset = '151'
  97. SiriAppSelection = '215'
  98. PrivateFederatedLearning = '304'
  99. Bluetooth = '118'
  100. SCPMotion = '143'
  101. HangSpin = '207'
  102. StepCount = '160'
  103. RTCTransparency = '224'
  104. DiagnosticRequest = '312'
  105. MemorySnapshot = '152'
  106. Rosetta_B = '216'
  107. AudioAccessory = '305'
  108. General = '119'
  109. HotSpotIOMicroSS = '144'
  110. GeoServicesTransparency = '233'
  111. MotionState = '161'
  112. AppStoreTransparency = '225'
  113. SiriSearchFeedback = '313'
  114. BearTrapReserved = '153'
  115. Portrait = '217'
  116. AWDMetricLog = 'metriclog'
  117. SymptomsIO = '145'
  118. SubmissionReserved = '170'
  119. WifiCrash = '209'
  120. Natalies = '162'
  121. SecurityTransparency = '226'
  122. BiomeMapReduce = '234'
  123. MemoryGraph = '154'
  124. MultichannelAudio = '218'
  125. honeybee_payload = '146'
  126. MesaReserved = '171'
  127. WifiSensing = '235'
  128. SiriMiss = '163'
  129. ExcResourceThreads_227 = '227'
  130. TestA = 'T01'
  131. NetworkUsage = '155'
  132. WifiReserved = '180'
  133. SiriActionPrediction = '219'
  134. honeybee_heartbeat = '147'
  135. ECCEvent = '172'
  136. KeyTransparency = '236'
  137. SubDiagHeartBeat = '164'
  138. ThirdPartyHang = '228'
  139. OSFault = '308'
  140. CoreTime = '156'
  141. WifiDriverReserved = '181'
  142. Crash_309 = '309'
  143. honeybee_issue = '148'
  144. CellularPerfReserved = '173'
  145. TestB = 'T02'
  146. StorageStatus = '165'
  147. SiriNotificationTransparency = '229'
  148. TestC = 'T03'
  149. CPUMicroSS = '157'
  150. AccessoryUpdate = '182'
  151. xprotect = '20'
  152. MultitouchFirmware = '149'
  153. MicroStackshot = '174'
  154. AppLaunchDiagnostics = '238'
  155. KeyboardAccuracy = '166'
  156. GPURestart = '21'
  157. FaceTime = '191'
  158. DuetKnowledgeCollector = '158'
  159. OTASUpdate = '183'
  160. ExcResourceThreads_327 = '327'
  161. ExcResource_22 = '22'
  162. DuetDB = '175'
  163. ThirdPartyHangDeveloper = '328'
  164. PrivacySettings = '167'
  165. GasGauge = '192'
  166. MicroStackShots = '23'
  167. BasebandCrash = '159'
  168. GPURestart_184 = '184'
  169. SystemWatchdogCrash = '409'
  170. FlashStatus = '176'
  171. SleepWakeFailure = '24'
  172. CarouselEvent = '168'
  173. AggregateD = '193'
  174. WakeupsMonitorViolation = '25'
  175. DifferentialPrivacy_50 = '50'
  176. ExcResource_185 = '185'
  177. UIAutomation = '177'
  178. ping = '26'
  179. SiriTransaction = '169'
  180. SURestore = '194'
  181. KtraceStackshot = '186'
  182. WirelessDiagnostics = '27'
  183. PowerLogLite = '178'
  184. SKAdNetworkAnalytics = '237'
  185. HangWorkflowResponsiveness = '239'
  186. CompositorClientHang = '243'
  187. class CrashReportBase:
  188. def __init__(self, metadata: Mapping, data: str, filename: str = None):
  189. self.filename = filename
  190. self._metadata = metadata
  191. self._data = data
  192. self._parse()
  193. def _parse(self):
  194. self._is_json = False
  195. try:
  196. modified_data = self._data
  197. if '\n \n' in modified_data:
  198. modified_data, rest = modified_data.split('\n \n', 1)
  199. rest = '",' + rest.split('",', 1)[1]
  200. modified_data += rest
  201. self._data = json.loads(modified_data)
  202. self._is_json = True
  203. except json.decoder.JSONDecodeError:
  204. pass
  205. @cached_property
  206. def bug_type(self) -> BugType:
  207. return BugType(self.bug_type_str)
  208. @cached_property
  209. def bug_type_str(self) -> str:
  210. return self._metadata['bug_type']
  211. @cached_property
  212. def incident_id(self):
  213. return self._metadata.get('incident_id')
  214. @cached_property
  215. def timestamp(self) -> datetime:
  216. timestamp = self._metadata.get('timestamp')
  217. timestamp_without_timezone = timestamp.rsplit(' ', 1)[0]
  218. return datetime.strptime(timestamp_without_timezone, '%Y-%m-%d %H:%M:%S.%f')
  219. @cached_property
  220. def name(self) -> str:
  221. return self._metadata.get('name')
  222. def __repr__(self) -> str:
  223. filename = ''
  224. if self.filename:
  225. filename = f'FILENAME:{posixpath.basename(self.filename)} '
  226. return f'<{self.__class__} {filename}TIMESTAMP:{self.timestamp}>'
  227. def __str__(self) -> str:
  228. filename = ''
  229. if self.filename:
  230. filename = self.filename
  231. return cyan(f'{self.incident_id} {self.timestamp}\n{filename}\n\n')
  232. class UserModeCrashReport(CrashReportBase):
  233. def _parse_field(self, name: str) -> str:
  234. name += ':'
  235. for line in self._data.split('\n'):
  236. if line.startswith(name):
  237. field = line.split(name, 1)[1]
  238. field = field.strip()
  239. return field
  240. @cached_property
  241. def faulting_thread(self) -> int:
  242. if self._is_json:
  243. return self._data['faultingThread']
  244. else:
  245. return int(self._parse_field('Triggered by Thread'))
  246. @cached_property
  247. def frames(self) -> List[Frame]:
  248. result = []
  249. if self._is_json:
  250. thread_index = self.faulting_thread
  251. images = self._data['usedImages']
  252. for frame in self._data['threads'][thread_index]['frames']:
  253. image = images[frame['imageIndex']]
  254. result.append(
  255. Frame(image_name=image.get('path'), image_base=image.get('base'), symbol=frame.get('symbol'),
  256. image_offset=frame.get('imageOffset'), symbol_offset=frame.get('symbolLocation')))
  257. else:
  258. in_frames = False
  259. for line in self._data.split('\n'):
  260. if in_frames:
  261. splitted = line.split()
  262. if len(splitted) == 0:
  263. break
  264. assert splitted[-2] == '+'
  265. image_base = splitted[-3]
  266. if image_base.startswith('0x'):
  267. result.append(Frame(image_name=splitted[1], image_base=int(image_base, 16), symbol=None,
  268. image_offset=int(splitted[-1]), symbol_offset=None))
  269. else:
  270. # symbolicated
  271. result.append(Frame(image_name=splitted[1], image_base=None, symbol=image_base,
  272. image_offset=None, symbol_offset=int(splitted[-1])))
  273. if line.startswith(f'Thread {self.faulting_thread} Crashed:'):
  274. in_frames = True
  275. return result
  276. @cached_property
  277. def registers(self) -> List[Register]:
  278. result = []
  279. if self._is_json:
  280. thread_index = self._data['faultingThread']
  281. thread_state = self._data['threads'][thread_index]['threadState']
  282. if 'x' in thread_state:
  283. for i, reg_x in enumerate(thread_state['x']):
  284. result.append(Register(name=f'x{i}', value=reg_x['value']))
  285. for i, (name, value) in enumerate(thread_state.items()):
  286. if name == 'x':
  287. for j, reg_x in enumerate(value):
  288. result.append(Register(name=f'x{j}', value=reg_x['value']))
  289. else:
  290. if isinstance(value, dict):
  291. result.append(Register(name=name, value=value['value']))
  292. else:
  293. in_frames = False
  294. for line in self._data.split('\n'):
  295. if in_frames:
  296. splitted = line.split()
  297. if len(splitted) == 0:
  298. break
  299. for i in range(0, len(splitted), 2):
  300. register_name = splitted[i]
  301. if not register_name.endswith(':'):
  302. break
  303. register_name = register_name[:-1]
  304. register_value = int(splitted[i + 1], 16)
  305. result.append(Register(name=register_name, value=register_value))
  306. if line.startswith(f'Thread {self.faulting_thread} crashed with ARM Thread State'):
  307. in_frames = True
  308. return result
  309. @cached_property
  310. def exception_type(self):
  311. if self._is_json:
  312. return self._data['exception'].get('type')
  313. else:
  314. return self._parse_field('Exception Type')
  315. @cached_property
  316. def exception_subtype(self) -> Optional[str]:
  317. if self._is_json:
  318. return self._data['exception'].get('subtype')
  319. else:
  320. return self._parse_field('Exception Subtype')
  321. @cached_property
  322. def application_specific_information(self) -> Optional[str]:
  323. result = ''
  324. if self._is_json:
  325. asi = self._data.get('asi')
  326. if asi is None:
  327. return None
  328. return asi
  329. else:
  330. in_frames = False
  331. for line in self._data.split('\n'):
  332. if in_frames:
  333. line = line.strip()
  334. if len(line) == 0:
  335. break
  336. result += line + '\n'
  337. if line.startswith('Application Specific Information:'):
  338. in_frames = True
  339. result = result.strip()
  340. if not result:
  341. return None
  342. return result
  343. def __str__(self) -> str:
  344. result = super().__str__()
  345. result += bold(f'Exception: {self.exception_type}\n')
  346. if self.exception_subtype:
  347. result += bold('Exception Subtype: ')
  348. result += f'{self.exception_subtype}\n'
  349. if self.application_specific_information:
  350. result += bold('Application Specific Information: ')
  351. result += str(self.application_specific_information)
  352. result += '\n'
  353. result += bold('Registers:')
  354. for i, register in enumerate(self.registers):
  355. if i % 4 == 0:
  356. result += '\n'
  357. result += f'{register.name} = 0x{register.value:016x} '.rjust(30)
  358. result += '\n\n'
  359. result += bold('Frames:\n')
  360. for frame in self.frames:
  361. image_base = '_HEADER'
  362. if frame.image_base is not None:
  363. image_base = f'0x{frame.image_base:x}'
  364. result += f'\t[{frame.image_name}] {image_base}'
  365. if frame.image_offset:
  366. result += f' + 0x{frame.image_offset:x}'
  367. if frame.symbol is not None:
  368. result += f' ({frame.symbol} + 0x{frame.symbol_offset:x})'
  369. result += '\n'
  370. return result
  371. def get_crash_report_from_file(crash_report_file: IO) -> CrashReportBase:
  372. metadata = json.loads(crash_report_file.readline())
  373. try:
  374. bug_type = BugType(metadata['bug_type'])
  375. except ValueError:
  376. return CrashReportBase(metadata, crash_report_file.read(), crash_report_file.name)
  377. bug_type_parsers = {
  378. BugType.Crash_109: UserModeCrashReport,
  379. BugType.Crash_309: UserModeCrashReport,
  380. BugType.ExcResourceThreads_327: UserModeCrashReport,
  381. BugType.ExcResource_385: UserModeCrashReport,
  382. }
  383. parser = bug_type_parsers.get(bug_type)
  384. if parser is None:
  385. return CrashReportBase(metadata, crash_report_file.read(), crash_report_file.name)
  386. return parser(metadata, crash_report_file.read(), crash_report_file.name)
  387. if __name__ == '__main__':
  388. with open(sys.argv[-1]) as f:
  389. print(get_crash_report_from_file(f))