File indexing completed on 2024-04-28 05:26:53

0001 # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 # SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0003 
0004 import unittest
0005 from unittest.mock import MagicMock, patch
0006 from chai import Chai
0007 import sys
0008 import os
0009 from pathlib import Path
0010 
0011 os.environ['DRKONQI_VERSION'] = '1.2.3'
0012 
0013 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
0014 sys.path.append(f'{SCRIPT_DIR}/python/')
0015 sys.path.append(f'{os.path.dirname(SCRIPT_DIR)}/data/')
0016 
0017 import gdb_preamble.preamble as preamble
0018 import gdb
0019 
0020 class PreambleTest(Chai):
0021     @classmethod
0022     def setUpClass(self):
0023         pass
0024 
0025     def setUp(self):
0026         preamble.SentryImage._objfiles = {}
0027         super(PreambleTest, self).setUp()
0028 
0029     def frame(self):
0030         self.mock(gdb, 'solib_name')
0031         self.expect(gdb.solib_name).args(0x1).returns(
0032             f'{Path.home()}/foo.so').at_least(0)
0033 
0034         symbol = MagicMock()
0035         symbol.__str__.return_value = 'i'
0036         symbol.value.return_value = 123
0037 
0038         symtab = MagicMock()
0039         symtab.fullname = MagicMock(return_value=f'{Path.home()}/foo.so')
0040 
0041         sal = MagicMock()
0042         sal.symtab = symtab
0043         sal.line = 0
0044 
0045         rax = self.mock(MagicMock)
0046         rax.name = "rax"
0047         self.expect(rax, 'startswith').args('ymm').returns(False).at_least(0)
0048 
0049         ymm0 = self.mock(MagicMock)
0050         ymm0.name = "ymm0"
0051         self.expect(ymm0, 'startswith').args('ymm').returns(True).at_least(0)
0052 
0053         architecture = self.mock()
0054         self.expect(architecture, 'registers').returns(['ymm0', rax]).at_least(0)
0055 
0056         rax_value = self.mock()
0057         self.expect(rax_value, 'format_string').args(format='x').returns('0x2').at_least(0)
0058 
0059         f = self.mock()
0060         self.expect(f, 'block').returns([symbol]).at_least(0)
0061         self.expect(f, 'find_sal').returns(sal).at_least(0)
0062         self.expect(f, 'name').returns('main').at_least(0)
0063         self.expect(f, 'pc').returns(0x1).at_least(0)
0064         self.expect(f, 'architecture').returns(architecture).at_least(0)
0065         self.expect(f, 'read_register').args(rax).returns(rax_value).at_least(0)
0066         self.expect(f, 'read_register').args(ymm0).times(0) # never
0067         self.expect(f, 'type').returns(0).at_least(0) # NORMAL_FRAME
0068         return f
0069 
0070     def test_sentry_variables(self):
0071         variables = preamble.SentryVariables(self.frame())
0072         self.assert_equal({'i': '123'}, variables.to_dict())
0073 
0074     def test_sentry_frame(self):
0075         sentry_frame = preamble.SentryFrame(self.frame())
0076         self.assert_equal({'filename': '$HOME/foo.so',
0077                            'function': 'main',
0078                            'instruction_addr': '0x1',
0079                            'lineno': 1, # NOTE: sentry starts at 1, gdb at 0
0080                            'package': '$HOME/foo.so',
0081                            'vars': {'i': '123'}}, sentry_frame.to_dict())
0082 
0083     def test_sentry_registers(self):
0084         registers = preamble.SentryRegisters(self.frame())
0085         self.assert_equal({'rax': '0x2'}, registers.to_dict())
0086 
0087     def test_sentry_thread(self):
0088         the_frame = self.frame()
0089 
0090         thread = self.mock()
0091         self.expect(thread.switch)
0092         thread.ptid = [123, 456, 789]
0093         thread.name = "TheThread"
0094         self.expect(thread.is_exited).returns(False)
0095 
0096         self.mock(gdb, 'newest_frame')
0097         self.expect(gdb.newest_frame).returns(the_frame)
0098         gdb.SIGTRAMP_FRAME = 4
0099 
0100         self.mock(gdb, 'FrameIterator')
0101         self.mock(gdb, 'FrameIterator.FrameIterator')
0102         self.expect(gdb.FrameIterator.FrameIterator).args(the_frame).returns([the_frame])
0103 
0104         thread = preamble.SentryThread(thread, is_crashed=False)
0105         self.assert_equal({'crashed': False,
0106                            'current': False,
0107                            'held_locks': {},
0108                            'state': None,
0109                             'id': 456,
0110                             'main': False,
0111                             'name': 'TheThread',
0112                             'stacktrace': {'frames': [{'filename': '$HOME/foo.so',
0113                                                         'function': 'main',
0114                                                         'instruction_addr': '0x1',
0115                                                         'lineno': 1, # NOTE: sentry starts at 1, gdb at 0
0116                                                         'package': '$HOME/foo.so',
0117                                                         'vars': {'i': '123'}}],
0118                                             'registers': {'rax': '0x2'}}}, thread.to_dict())
0119 
0120     def test_sentry_images(self):
0121         proc_mappings = """
0122 process 207700
0123 Mapped address spaces:
0124 
0125           Start Addr           End Addr       Size     Offset  Perms  objfile
0126       0x555555554000     0x555555556000     0x2000        0x0  r--p   /usr/bin/kwrite
0127       0x555555556000     0x555555558000     0x2000     0x2000  r-xp   /usr/bin/kwrite
0128       0x555555558000     0x555555559000     0x1000     0x4000  r--p   /usr/bin/kwrite
0129       0x55555555a000     0x55555555b000     0x1000     0x5000  r--p   /usr/bin/kwrite
0130       0x55555555b000     0x55555555c000     0x1000     0x6000  rw-p   /usr/bin/kwrite
0131       0x55555555c000     0x55555637e000   0xe22000        0x0  rw-p   [heap]
0132       0x7fffc0000000     0x7fffc0021000    0x21000        0x0  rw-p
0133       0x7fffc0021000     0x7fffc4000000  0x3fdf000        0x0  ---p
0134       0x7fffc7000000     0x7fffc703c000    0x3c000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libx265.so.199
0135       0x7fffc703c000     0x7fffc7ed4000   0xe98000    0x3c000  r-xp   /usr/lib/x86_64-linux-gnu/libx265.so.199
0136       0x7fffc7ed4000     0x7fffc7f4f000    0x7b000   0xed4000  r--p   /usr/lib/x86_64-linux-gnu/libx265.so.199
0137       0x7fffc7f4f000     0x7fffc7f52000     0x3000   0xf4e000  r--p   /usr/lib/x86_64-linux-gnu/libx265.so.199
0138       0x7fffc7f52000     0x7fffc7f55000     0x3000   0xf51000  rw-p   /usr/lib/x86_64-linux-gnu/libx265.so.199
0139       0x7fffd5ff6000     0x7fffd5ffc000     0x6000        0x0  r--s   /var/cache/fontconfig/68a526f6-7ccc-4a32-af00-647cd10884c1-le64.cache-7
0140       0x7fffdc000000     0x7fffdc001000     0x1000        0x0  r--s   /home/me/.local/share/mime/mime.cache
0141       0x7fffdc476000     0x7fffdc676000   0x200000 0x118ded000  rw-s   /dev/dri/renderD128
0142       0x7ffff6200000     0x7ffff6228000    0x28000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
0143       0x7ffff6228000     0x7ffff63bd000   0x195000    0x28000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
0144       0x7ffff63bd000     0x7ffff6415000    0x58000   0x1bd000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
0145       0x7ffff6415000     0x7ffff6419000     0x4000   0x214000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
0146       0x7ffff6419000     0x7ffff641b000     0x2000   0x218000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
0147       0x7ffff7fbd000     0x7ffff7fc1000     0x4000        0x0  r--p   [vvar]
0148       0x7ffff7fc1000     0x7ffff7fc3000     0x2000        0x0  r-xp   [vdso]
0149       0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0150       0x7ffff7fc5000     0x7ffff7fef000    0x2a000     0x2000  r-xp   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0151       0x7ffff7fef000     0x7ffff7ffa000     0xb000    0x2c000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0152       0x7ffff7ffa000     0x7ffff7ffb000     0x1000 0x10663c000  rw-s   /dev/dri/renderD128
0153       0x7ffff7ffb000     0x7ffff7ffd000     0x2000    0x37000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0154       0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x39000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0155       0x7ffffffdd000     0x7ffffffff000    0x22000        0x0  rw-p   [stack]
0156   0xffffffffff600000 0xffffffffff601000     0x1000        0x0  --xp   [vsyscall]
0157         """
0158 
0159         self.mock(gdb, 'execute')
0160         self.expect(gdb.execute).args('info proc mappings', to_string=True).returns(proc_mappings)
0161 
0162         obj1 = self.mock()
0163         obj1.filename = '/usr/lib/x86_64-linux-gnu/libc.so.6'
0164         obj1.build_id = '161dd025ad435f0e873aafd55dc65d8a4cb1d93f'
0165 
0166         obj2 = self.mock()
0167         obj2.filename = '/usr/lib/x86_64-linux-gnu/libx265.so.199'
0168         obj2.build_id = '272ee025ad435f0e873aafd55dc65d8a4cb1d93f'
0169 
0170         self.mock(gdb, 'objfiles')
0171         self.expect(gdb.objfiles).returns([obj1, obj2])
0172 
0173         images = preamble.SentryImages()
0174         self.assert_equal([{'arch': 'x86_64',
0175             'code_file': '/usr/lib/x86_64-linux-gnu/libx265.so.199',
0176             'code_id': '272ee025ad435f0e873aafd55dc65d8a4cb1d93f',
0177             'debug_id': '25e02e27-43ad-0e5f-873a-afd55dc65d8a',
0178             'image_addr': '0x7fffc7000000',
0179             'image_size': 16076800,
0180             'type': 'elf'},
0181             {'arch': 'x86_64',
0182             'code_file': '/usr/lib/x86_64-linux-gnu/libc.so.6',
0183             'code_id': '161dd025ad435f0e873aafd55dc65d8a4cb1d93f',
0184             'debug_id': '25d01d16-43ad-0e5f-873a-afd55dc65d8a',
0185             'image_addr': '0x7ffff6200000',
0186             'image_size': 2207744,
0187             'type': 'elf'}]
0188             , images.to_list())
0189 
0190     def test_sentry_qml(self):
0191         thread = self.mock()
0192         self.expect(thread.is_valid).returns(True)
0193         self.expect(thread.switch)
0194 
0195         inferior = self.mock()
0196         self.expect(inferior.threads).returns([thread])
0197 
0198         symbol0 = self.mock()
0199         symbol0.is_variable = False
0200         symbol0.is_argument = False
0201 
0202         frame0 = self.mock(MagicMock)
0203 
0204         dereferenced_type = self.mock()
0205         dereferenced_type.name = 'QV4::ExecutionEngine'
0206 
0207         typeobj_target = self.mock()
0208         self.expect(typeobj_target.unqualified).returns(dereferenced_type)
0209 
0210         typeobj = self.mock()
0211         typeobj.code = 1
0212         self.expect(typeobj.target).returns(typeobj_target)
0213 
0214         value1 = MagicMock()
0215         value1.is_optimized_out = False
0216         value1.type = typeobj
0217         value1.__int__.return_value = 0x3
0218 
0219         symbol1 = self.mock()
0220         symbol1.is_variable = True
0221         symbol1.is_argument = False
0222         self.expect(symbol1.value).args(frame0).returns(value1)
0223 
0224         self.expect(frame0.__len__).returns(5)
0225         self.expect(frame0.block).returns([symbol0, symbol1])
0226 
0227         frame1 = self.mock(MagicMock)
0228         self.expect(frame1.__len__).returns(5)
0229         self.expect(frame1.older).returns(frame0)
0230 
0231         self.mock(gdb, 'selected_inferior')
0232         self.expect(gdb.selected_inferior).returns(inferior)
0233         self.mock(gdb, 'selected_thread')
0234         self.expect(gdb.selected_thread).returns(thread)
0235         self.mock(gdb, 'newest_frame')
0236         self.expect(gdb.newest_frame).returns(frame1)
0237         self.mock(gdb, 'parse_and_eval')
0238         qml_trace = '''
0239 "stack=[frame={level=\\"0\\",func=\\"expression for onCompleted\\",file=\\"qrc:/ui/main.qml\\",fullname=\\"qrc:/ui/main.qml\\",line=\\"10\\",language=\\"js\\"}]"
0240         '''
0241         self.expect(gdb.parse_and_eval).args('qt_v4StackTraceForEngine((void*)0x3)').returns(qml_trace)
0242         gdb.TYPE_CODE_PTR = 1
0243 
0244         thread = preamble.SentryQMLThread()
0245         self.assert_equal({'crashed': True,
0246             'id': 'QML',
0247             'name': 'QML',
0248             'stacktrace': {'frames': [{'filename': 'qrc:/ui/main.qml',
0249             'function': 'expression for onCompleted',
0250             'in_app': True,
0251             'lineno': 10,
0252             'platform': 'other'}]}}, thread.to_dict())
0253 
0254     def test_sentry_image(self):
0255         objfile = self.mock()
0256         objfile.filename = '/usr/bin/true'
0257         objfile.build_id = '272ee025ad435f0e873aafd55dc65d8a4cb1d93f'
0258 
0259         self.mock(gdb, 'objfiles')
0260         self.expect(gdb.objfiles).returns([objfile])
0261 
0262         image = preamble.SentryImage('/usr/bin/true', 1000, 2000)
0263         self.assert_equal({'arch': 'x86_64',
0264             'code_file': '/usr/bin/true',
0265             'code_id': '272ee025ad435f0e873aafd55dc65d8a4cb1d93f',
0266             'debug_id': '25e02e27-43ad-0e5f-873a-afd55dc65d8a',
0267             'image_addr': '0x3e8',
0268             'image_size': 1000,
0269             'type': 'elf'}, image.to_dict())
0270 
0271     def test_sentry_image_mapping_fail(self):
0272         try:
0273             import sentry_sdk
0274 
0275             objfile = self.mock()
0276             objfile.filename = '/usr/bin/true'
0277             objfile.build_id = '272ee025ad435f0e873aafd55dc65d8a4cb1d93f'
0278 
0279             self.mock(gdb, 'objfiles')
0280             self.expect(gdb.objfiles).returns([objfile])
0281 
0282             self.mock(gdb, 'lookup_objfile')
0283             self.expect(gdb.lookup_objfile).returns(None)
0284 
0285             self.mock(sentry_sdk, 'capture_exception')
0286             self.expect(sentry_sdk.capture_exception)
0287             preamble.SentryImage('/usr/bin/true.so', 1000, 2000)
0288         except Exception as err:
0289             pass
0290 
0291     def test_print_qml_frames(self):
0292         # the payload is missing a line -> we expect no assertions of any kind!
0293         preamble.print_qml_frames('frame={level="0",func="saveConfig",file="/data/projects/kde/usr/share/plasma/shells/org.kde.plasma.desktop/contents/configuration/ConfigurationContainmentAppearance.qml",fullname="/data/project".')
0294 
0295 if __name__ == '__main__':
0296     unittest.main()
0297