1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2017, Linaro Limited
5#
6
7
8import argparse
9import errno
10import glob
11import os
12import re
13import subprocess
14import sys
15import termios
16
17CALL_STACK_RE = re.compile('Call stack:')
18TEE_LOAD_ADDR_RE = re.compile(r'TEE load address @ (?P<load_addr>0x[0-9a-f]+)')
19# This gets the address from lines looking like this:
20# E/TC:0  0x001044a8
21STACK_ADDR_RE = re.compile(
22    r'[UEIDFM]/(TC|LD):(\?*|[0-9]*) [0-9]* +(?P<addr>0x[0-9a-f]+)')
23ABORT_ADDR_RE = re.compile(r'-abort at address (?P<addr>0x[0-9a-f]+)')
24TA_PANIC_RE = re.compile(r'TA panicked with code (?P<code>0x[0-9a-f]+)')
25REGION_RE = re.compile(r'region +[0-9]+: va (?P<addr>0x[0-9a-f]+) '
26                       r'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)'
27                       r'( flags .{4} (\[(?P<elf_idx>[0-9]+)\])?)?')
28ELF_LIST_RE = re.compile(r'\[(?P<idx>[0-9]+)\] (?P<uuid>[0-9a-f\-]+)'
29                         r' @ (?P<load_addr>0x[0-9a-f\-]+)')
30FUNC_GRAPH_RE = re.compile(r'Function graph')
31GRAPH_ADDR_RE = re.compile(r'(?P<addr>0x[0-9a-f]+)')
32GRAPH_RE = re.compile(r'}')
33
34epilog = '''
35This scripts reads an OP-TEE abort or panic message from stdin and adds debug
36information to the output, such as '<function> at <file>:<line>' next to each
37address in the call stack. Any message generated by OP-TEE and containing a
38call stack can in principle be processed by this script. This currently
39includes aborts and panics from the TEE core as well as from any TA.
40The paths provided on the command line are used to locate the appropriate ELF
41binary (tee.elf or Trusted Application). The GNU binutils (addr2line, objdump,
42nm) are used to extract the debug info. If the CROSS_COMPILE environment
43variable is set, it is used as a prefix to the binutils tools. That is, the
44script will invoke $(CROSS_COMPILE)addr2line etc. If it is not set however,
45the prefix will be determined automatically for each ELF file based on its
46architecture (arm-linux-gnueabihf-, aarch64-linux-gnu-). The resulting command
47is then expected to be found in the user's PATH.
48
49OP-TEE abort and panic messages are sent to the secure console. They look like
50the following:
51
52  E/TC:0 User TA data-abort at address 0xffffdecd (alignment fault)
53  ...
54  E/TC:0 Call stack:
55  E/TC:0  0x4000549e
56  E/TC:0  0x40001f4b
57  E/TC:0  0x4000273f
58  E/TC:0  0x40005da7
59
60Inspired by a script of the same name by the Chromium project.
61
62Sample usage:
63
64  $ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/*
65  <paste whole dump here>
66  ^D
67
68Also, this script reads function graph generated for OP-TEE user TA from
69/tmp/ftrace-<ta_uuid>.out file and resolves function addresses to corresponding
70symbols.
71
72Sample usage:
73
74  $ cat /tmp/ftrace-<ta_uuid>.out | scripts/symbolize.py -d <ta_uuid>.elf
75  <paste function graph here>
76  ^D
77'''
78
79tee_result_names = {
80        '0xf0100001': 'TEE_ERROR_CORRUPT_OBJECT',
81        '0xf0100002': 'TEE_ERROR_CORRUPT_OBJECT_2',
82        '0xf0100003': 'TEE_ERROR_STORAGE_NOT_AVAILABLE',
83        '0xf0100004': 'TEE_ERROR_STORAGE_NOT_AVAILABLE_2',
84        '0xf0100006': 'TEE_ERROR_CIPHERTEXT_INVALID ',
85        '0xffff0000': 'TEE_ERROR_GENERIC',
86        '0xffff0001': 'TEE_ERROR_ACCESS_DENIED',
87        '0xffff0002': 'TEE_ERROR_CANCEL',
88        '0xffff0003': 'TEE_ERROR_ACCESS_CONFLICT',
89        '0xffff0004': 'TEE_ERROR_EXCESS_DATA',
90        '0xffff0005': 'TEE_ERROR_BAD_FORMAT',
91        '0xffff0006': 'TEE_ERROR_BAD_PARAMETERS',
92        '0xffff0007': 'TEE_ERROR_BAD_STATE',
93        '0xffff0008': 'TEE_ERROR_ITEM_NOT_FOUND',
94        '0xffff0009': 'TEE_ERROR_NOT_IMPLEMENTED',
95        '0xffff000a': 'TEE_ERROR_NOT_SUPPORTED',
96        '0xffff000b': 'TEE_ERROR_NO_DATA',
97        '0xffff000c': 'TEE_ERROR_OUT_OF_MEMORY',
98        '0xffff000d': 'TEE_ERROR_BUSY',
99        '0xffff000e': 'TEE_ERROR_COMMUNICATION',
100        '0xffff000f': 'TEE_ERROR_SECURITY',
101        '0xffff0010': 'TEE_ERROR_SHORT_BUFFER',
102        '0xffff0011': 'TEE_ERROR_EXTERNAL_CANCEL',
103        '0xffff300f': 'TEE_ERROR_OVERFLOW',
104        '0xffff3024': 'TEE_ERROR_TARGET_DEAD',
105        '0xffff3041': 'TEE_ERROR_STORAGE_NO_SPACE',
106        '0xffff3071': 'TEE_ERROR_MAC_INVALID',
107        '0xffff3072': 'TEE_ERROR_SIGNATURE_INVALID',
108        '0xffff5000': 'TEE_ERROR_TIME_NOT_SET',
109        '0xffff5001': 'TEE_ERROR_TIME_NEEDS_RESET',
110    }
111
112
113def get_args():
114    parser = argparse.ArgumentParser(
115        formatter_class=argparse.RawDescriptionHelpFormatter,
116        description='Symbolizes OP-TEE abort dumps or function graphs',
117        epilog=epilog)
118    parser.add_argument('-d', '--dir', action='append', nargs='+',
119                        help='Search for ELF file in DIR. tee.elf is needed '
120                        'to decode a TEE Core or pseudo-TA abort, while '
121                        '<TA_uuid>.elf is required if a user-mode TA has '
122                        'crashed. For convenience, ELF files may also be '
123                        'given.')
124    parser.add_argument('-s', '--strip_path', nargs='?',
125                        help='Strip STRIP_PATH from file paths (default: '
126                        'current directory, use -s with no argument to show '
127                        'full paths)', default=os.getcwd())
128
129    return parser.parse_args()
130
131
132class Symbolizer(object):
133    def __init__(self, out, dirs, strip_path):
134        self._out = out
135        self._dirs = dirs
136        self._strip_path = strip_path
137        self._addr2line = None
138        self.reset()
139
140    def my_Popen(self, cmd):
141        try:
142            return subprocess.Popen(cmd, stdin=subprocess.PIPE,
143                                    stdout=subprocess.PIPE,
144                                    universal_newlines=True,
145                                    bufsize=1)
146        except OSError as e:
147            if e.errno == errno.ENOENT:
148                print("*** Error:{}: command not found".format(cmd[0]),
149                      file=sys.stderr)
150                sys.exit(1)
151
152    def get_elf(self, elf_or_uuid):
153        if not elf_or_uuid.endswith('.elf'):
154            elf_or_uuid += '.elf'
155        for d in self._dirs:
156            if d.endswith(elf_or_uuid) and os.path.isfile(d):
157                return d
158            elf = glob.glob(d + '/' + elf_or_uuid)
159            if elf:
160                return elf[0]
161
162    def set_arch(self, elf):
163        self._arch = os.getenv('CROSS_COMPILE')
164        if self._arch:
165            return
166        p = subprocess.Popen(['file', '-L', elf], stdout=subprocess.PIPE)
167        output = p.stdout.readlines()
168        p.terminate()
169        if b'ARM aarch64,' in output[0]:
170            self._arch = 'aarch64-linux-gnu-'
171        elif b'ARM,' in output[0]:
172            self._arch = 'arm-linux-gnueabihf-'
173
174    def arch_prefix(self, cmd, elf):
175        self.set_arch(elf)
176        if self._arch is None:
177            return ''
178        return self._arch + cmd
179
180    def spawn_addr2line(self, elf_name):
181        if elf_name is None:
182            return
183        if self._addr2line_elf_name is elf_name:
184            return
185        if self._addr2line:
186            self._addr2line.terminate
187            self._addr2line = None
188        elf = self.get_elf(elf_name)
189        if not elf:
190            return
191        cmd = self.arch_prefix('addr2line', elf)
192        if not cmd:
193            return
194        self._addr2line = self.my_Popen([cmd, '-f', '-p', '-e', elf])
195        self._addr2line_elf_name = elf_name
196
197    # If addr falls into a region that maps a TA ELF file, return the load
198    # address of that file.
199    def elf_load_addr(self, addr):
200        if self._regions:
201            for r in self._regions:
202                r_addr = int(r[0], 16)
203                r_size = int(r[1], 16)
204                i_addr = int(addr, 16)
205                if (i_addr >= r_addr and i_addr < (r_addr + r_size)):
206                    # Found region
207                    elf_idx = r[2]
208                    if elf_idx is not None:
209                        return self._elfs[int(elf_idx)][1]
210            # In case address is not found in TA ELF file, fallback to tee.elf
211            # especially to symbolize mixed (user-space and kernel) addresses
212            # which is true when syscall ftrace is enabled along with TA
213            # ftrace.
214            return self._tee_load_addr
215        else:
216            # tee.elf
217            return self._tee_load_addr
218
219    def elf_for_addr(self, addr):
220        l_addr = self.elf_load_addr(addr)
221        if l_addr == self._tee_load_addr:
222            return 'tee.elf'
223        for k in self._elfs:
224            e = self._elfs[k]
225            if int(e[1], 16) == int(l_addr, 16):
226                return e[0]
227        return None
228
229    def subtract_load_addr(self, addr):
230        l_addr = self.elf_load_addr(addr)
231        if l_addr is None:
232            return None
233        if int(l_addr, 16) > int(addr, 16):
234            return ''
235        return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16))
236
237    def resolve(self, addr):
238        reladdr = self.subtract_load_addr(addr)
239        self.spawn_addr2line(self.elf_for_addr(addr))
240        if not reladdr or not self._addr2line:
241            return '???'
242        if self.elf_for_addr(addr) == 'tee.elf':
243            reladdr = '0x{:x}'.format(int(reladdr, 16) +
244                                      int(self.first_vma('tee.elf'), 16))
245        try:
246            print(reladdr, file=self._addr2line.stdin)
247            ret = self._addr2line.stdout.readline().rstrip('\n')
248        except IOError:
249            ret = '!!!'
250        return ret
251
252    # Armv8.5 with Memory Tagging Extension (MTE)
253    def strip_armv85_mte_tag(self, addr):
254        i_addr = int(addr, 16)
255        i_addr &= ~(0xf << 56)
256        return '0x{:x}'.format(i_addr)
257
258    def symbol_plus_offset(self, addr):
259        ret = ''
260        prevsize = 0
261        addr = self.strip_armv85_mte_tag(addr)
262        reladdr = self.subtract_load_addr(addr)
263        elf_name = self.elf_for_addr(addr)
264        if elf_name is None:
265            return ''
266        elf = self.get_elf(elf_name)
267        cmd = self.arch_prefix('nm', elf)
268        if not reladdr or not elf or not cmd:
269            return ''
270        ireladdr = int(reladdr, 16)
271        nm = self.my_Popen([cmd, '--numeric-sort', '--print-size', elf])
272        for line in iter(nm.stdout.readline, ''):
273            try:
274                addr, size, _, name = line.split()
275            except ValueError:
276                # Size is missing
277                try:
278                    addr, _, name = line.split()
279                    size = '0'
280                except ValueError:
281                    # E.g., undefined (external) symbols (line = "U symbol")
282                    continue
283            iaddr = int(addr, 16)
284            isize = int(size, 16)
285            if iaddr == ireladdr:
286                ret = name
287                break
288            if iaddr < ireladdr and iaddr + isize >= ireladdr:
289                offs = ireladdr - iaddr
290                ret = name + '+' + str(offs)
291                break
292            if iaddr > ireladdr and prevsize == 0:
293                offs = iaddr + ireladdr
294                ret = prevname + '+' + str(offs)
295                break
296            prevsize = size
297            prevname = name
298        nm.terminate()
299        return ret
300
301    def section_plus_offset(self, addr):
302        ret = ''
303        reladdr = self.subtract_load_addr(addr)
304        elf_name = self.elf_for_addr(addr)
305        if elf_name is None:
306            return ''
307        elf = self.get_elf(elf_name)
308        cmd = self.arch_prefix('objdump', elf)
309        if not reladdr or not elf or not cmd:
310            return ''
311        iaddr = int(reladdr, 16)
312        objdump = self.my_Popen([cmd, '--section-headers', elf])
313        for line in iter(objdump.stdout.readline, ''):
314            try:
315                idx, name, size, vma, lma, offs, algn = line.split()
316            except ValueError:
317                continue
318            ivma = int(vma, 16)
319            isize = int(size, 16)
320            if ivma == iaddr:
321                ret = name
322                break
323            if ivma < iaddr and ivma + isize >= iaddr:
324                offs = iaddr - ivma
325                ret = name + '+' + str(offs)
326                break
327        objdump.terminate()
328        return ret
329
330    def process_abort(self, line):
331        ret = ''
332        match = re.search(ABORT_ADDR_RE, line)
333        addr = match.group('addr')
334        pre = match.start('addr')
335        post = match.end('addr')
336        sym = self.symbol_plus_offset(addr)
337        sec = self.section_plus_offset(addr)
338        if sym or sec:
339            ret += line[:pre]
340            ret += addr
341            if sym:
342                ret += ' ' + sym
343            if sec:
344                ret += ' ' + sec
345            ret += line[post:]
346        return ret
347
348    # Return all ELF sections with the ALLOC flag
349    def read_sections(self, elf_name):
350        if elf_name is None:
351            return
352        if elf_name in self._sections:
353            return
354        elf = self.get_elf(elf_name)
355        if not elf:
356            return
357        cmd = self.arch_prefix('objdump', elf)
358        if not elf or not cmd:
359            return
360        self._sections[elf_name] = []
361        objdump = self.my_Popen([cmd, '--section-headers', elf])
362        for line in iter(objdump.stdout.readline, ''):
363            try:
364                _, name, size, vma, _, _, _ = line.split()
365            except ValueError:
366                if 'ALLOC' in line:
367                    self._sections[elf_name].append([name, int(vma, 16),
368                                                     int(size, 16)])
369
370    def first_vma(self, elf_name):
371        self.read_sections(elf_name)
372        return '0x{:x}'.format(self._sections[elf_name][0][1])
373
374    def overlaps(self, section, addr, size):
375        sec_addr = section[1]
376        sec_size = section[2]
377        if not size or not sec_size:
378            return False
379        return ((addr <= (sec_addr + sec_size - 1)) and
380                ((addr + size - 1) >= sec_addr))
381
382    def sections_in_region(self, addr, size, elf_idx):
383        ret = ''
384        addr = self.subtract_load_addr(addr)
385        if not addr:
386            return ''
387        iaddr = int(addr, 16)
388        isize = int(size, 16)
389        elf = self._elfs[int(elf_idx)][0]
390        if elf is None:
391            return ''
392        self.read_sections(elf)
393        if elf not in self._sections:
394            return ''
395        for s in self._sections[elf]:
396            if self.overlaps(s, iaddr, isize):
397                ret += ' ' + s[0]
398        return ret
399
400    def reset(self):
401        self._call_stack_found = False
402        if self._addr2line:
403            self._addr2line.terminate()
404            self._addr2line = None
405        self._addr2line_elf_name = None
406        self._arch = None
407        self._saved_abort_line = ''
408        self._sections = {}  # {elf_name: [[name, addr, size], ...], ...}
409        self._regions = []   # [[addr, size, elf_idx, saved line], ...]
410        self._elfs = {0: ["tee.elf", 0]}  # {idx: [uuid, load_addr], ...}
411        self._tee_load_addr = '0x0'
412        self._func_graph_found = False
413        self._func_graph_skip_line = True
414
415    def pretty_print_path(self, path):
416        if self._strip_path:
417            return re.sub(re.escape(self._strip_path) + '/*', '', path)
418        return path
419
420    def write(self, line):
421        if self._call_stack_found:
422            match = re.search(STACK_ADDR_RE, line)
423            if match:
424                addr = match.group('addr')
425                pre = match.start('addr')
426                post = match.end('addr')
427                self._out.write(line[:pre])
428                self._out.write(addr)
429                # The call stack contains return addresses (LR/ELR values).
430                # Heuristic: subtract 2 to obtain the call site of the function
431                # or the location of the exception. This value works for A64,
432                # A32 as well as Thumb.
433                pc = 0
434                lr = int(addr, 16)
435                if lr:
436                    pc = lr - 2
437                res = self.resolve('0x{:x}'.format(pc))
438                res = self.pretty_print_path(res)
439                self._out.write(' ' + res)
440                self._out.write(line[post:])
441                return
442            else:
443                self.reset()
444        if self._func_graph_found:
445            match = re.search(GRAPH_ADDR_RE, line)
446            match_re = re.search(GRAPH_RE, line)
447            if match:
448                addr = match.group('addr')
449                pre = match.start('addr')
450                post = match.end('addr')
451                self._out.write(line[:pre])
452                res = self.resolve(addr)
453                res_arr = re.split(' ', res)
454                self._out.write(res_arr[0])
455                self._out.write(line[post:])
456                self._func_graph_skip_line = False
457                return
458            elif match_re:
459                self._out.write(line)
460                return
461            elif self._func_graph_skip_line:
462                return
463            else:
464                self.reset()
465        match = re.search(REGION_RE, line)
466        if match:
467            # Region table: save info for later processing once
468            # we know which UUID corresponds to which ELF index
469            addr = match.group('addr')
470            size = match.group('size')
471            elf_idx = match.group('elf_idx')
472            self._regions.append([addr, size, elf_idx, line])
473            return
474        match = re.search(ELF_LIST_RE, line)
475        if match:
476            # ELF list: save info for later. Region table and ELF list
477            # will be displayed when the call stack is reached
478            i = int(match.group('idx'))
479            self._elfs[i] = [match.group('uuid'), match.group('load_addr'),
480                             line]
481            return
482        match = re.search(TA_PANIC_RE, line)
483        if match:
484            code = match.group('code')
485            if code in tee_result_names:
486                line = line.strip() + ' (' + tee_result_names[code] + ')\n'
487            self._out.write(line)
488            return
489        match = re.search(TEE_LOAD_ADDR_RE, line)
490        if match:
491            self._tee_load_addr = match.group('load_addr')
492        match = re.search(CALL_STACK_RE, line)
493        if match:
494            self._call_stack_found = True
495            if self._regions:
496                for r in self._regions:
497                    r_addr = r[0]
498                    r_size = r[1]
499                    elf_idx = r[2]
500                    saved_line = r[3]
501                    if elf_idx is None:
502                        self._out.write(saved_line)
503                    else:
504                        self._out.write(saved_line.strip() +
505                                        self.sections_in_region(r_addr,
506                                                                r_size,
507                                                                elf_idx) +
508                                        '\n')
509            if self._elfs:
510                for k in self._elfs:
511                    e = self._elfs[k]
512                    if (len(e) >= 3):
513                        # TA executable or library
514                        self._out.write(e[2].strip())
515                        elf = self.get_elf(e[0])
516                        if elf:
517                            rpath = os.path.realpath(elf)
518                            path = self.pretty_print_path(rpath)
519                            self._out.write(' (' + path + ')')
520                        self._out.write('\n')
521            # Here is a good place to resolve the abort address because we
522            # have all the information we need
523            if self._saved_abort_line:
524                self._out.write(self.process_abort(self._saved_abort_line))
525        match = re.search(FUNC_GRAPH_RE, line)
526        if match:
527            self._func_graph_found = True
528        match = re.search(ABORT_ADDR_RE, line)
529        if match:
530            self.reset()
531            # At this point the arch and TA load address are unknown.
532            # Save the line so We can translate the abort address later.
533            self._saved_abort_line = line
534        self._out.write(line)
535
536    def flush(self):
537        self._out.flush()
538
539
540def main():
541    args = get_args()
542    if args.dir:
543        # Flatten list in case -d is used several times *and* with multiple
544        # arguments
545        args.dirs = [item for sublist in args.dir for item in sublist]
546    else:
547        args.dirs = []
548    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
549
550    fd = sys.stdin.fileno()
551    isatty = os.isatty(fd)
552    if isatty:
553        old = termios.tcgetattr(fd)
554        new = termios.tcgetattr(fd)
555        new[3] = new[3] & ~termios.ECHO  # lflags
556    try:
557        if isatty:
558            termios.tcsetattr(fd, termios.TCSADRAIN, new)
559        for line in sys.stdin:
560            symbolizer.write(line)
561    finally:
562        symbolizer.flush()
563        if isatty:
564            termios.tcsetattr(fd, termios.TCSADRAIN, old)
565
566
567if __name__ == "__main__":
568    main()
569