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]*) [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. The resulting command is then expected to be found in the user's
47PATH.
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        elif b'RISC-V,' in output[0]:
174            if b'32-bit' in output[0]:
175                self._arch = 'riscv32-unknown-linux-gnu-'
176            elif b'64-bit' in output[0]:
177                self._arch = 'riscv64-unknown-linux-gnu-'
178
179    def arch_prefix(self, cmd, elf):
180        self.set_arch(elf)
181        if self._arch is None:
182            return ''
183        return self._arch + cmd
184
185    def spawn_addr2line(self, elf_name):
186        if elf_name is None:
187            return
188        if self._addr2line_elf_name is elf_name:
189            return
190        if self._addr2line:
191            self._addr2line.terminate
192            self._addr2line = None
193        elf = self.get_elf(elf_name)
194        if not elf:
195            return
196        cmd = self.arch_prefix('addr2line', elf)
197        if not cmd:
198            return
199        self._addr2line = self.my_Popen([cmd, '-f', '-p', '-e', elf])
200        self._addr2line_elf_name = elf_name
201
202    # If addr falls into a region that maps a TA ELF file, return the load
203    # address of that file.
204    def elf_load_addr(self, addr):
205        if self._regions:
206            for r in self._regions:
207                r_addr = int(r[0], 16)
208                r_size = int(r[1], 16)
209                i_addr = int(addr, 16)
210                if (i_addr >= r_addr and i_addr < (r_addr + r_size)):
211                    # Found region
212                    elf_idx = r[2]
213                    if elf_idx is not None:
214                        return self._elfs[int(elf_idx)][1]
215            # In case address is not found in TA ELF file, fallback to tee.elf
216            # especially to symbolize mixed (user-space and kernel) addresses
217            # which is true when syscall ftrace is enabled along with TA
218            # ftrace.
219            return self._tee_load_addr
220        else:
221            # tee.elf
222            return self._tee_load_addr
223
224    def elf_for_addr(self, addr):
225        l_addr = self.elf_load_addr(addr)
226        if l_addr == self._tee_load_addr:
227            return 'tee.elf'
228        for k in self._elfs:
229            e = self._elfs[k]
230            if int(e[1], 16) == int(l_addr, 16):
231                return e[0]
232        return None
233
234    def subtract_load_addr(self, addr):
235        l_addr = self.elf_load_addr(addr)
236        if l_addr is None:
237            return None
238        if int(l_addr, 16) > int(addr, 16):
239            return ''
240        return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16))
241
242    def resolve(self, addr):
243        reladdr = self.subtract_load_addr(addr)
244        self.spawn_addr2line(self.elf_for_addr(addr))
245        if not reladdr or not self._addr2line:
246            return '???'
247        if self.elf_for_addr(addr) == 'tee.elf':
248            reladdr = '0x{:x}'.format(int(reladdr, 16) +
249                                      int(self.first_vma('tee.elf'), 16))
250        try:
251            print(reladdr, file=self._addr2line.stdin)
252            ret = self._addr2line.stdout.readline().rstrip('\n')
253        except IOError:
254            ret = '!!!'
255        return ret
256
257    def symbol_plus_offset(self, addr):
258        ret = ''
259        prevsize = 0
260        reladdr = self.subtract_load_addr(addr)
261        elf_name = self.elf_for_addr(addr)
262        if elf_name is None:
263            return ''
264        elf = self.get_elf(elf_name)
265        if elf is None:
266            return ''
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        if elf is None:
309            return ''
310        cmd = self.arch_prefix('objdump', elf)
311        if not reladdr or not elf or not cmd:
312            return ''
313        iaddr = int(reladdr, 16)
314        objdump = self.my_Popen([cmd, '--section-headers', elf])
315        for line in iter(objdump.stdout.readline, ''):
316            try:
317                idx, name, size, vma, lma, offs, algn = line.split()
318            except ValueError:
319                continue
320            ivma = int(vma, 16)
321            isize = int(size, 16)
322            if ivma == iaddr:
323                ret = name
324                break
325            if ivma < iaddr and ivma + isize >= iaddr:
326                offs = iaddr - ivma
327                ret = name + '+' + str(offs)
328                break
329        objdump.terminate()
330        return ret
331
332    def process_abort(self, line):
333        ret = ''
334        match = re.search(ABORT_ADDR_RE, line)
335        addr = match.group('addr')
336        pre = match.start('addr')
337        post = match.end('addr')
338        sym = self.symbol_plus_offset(addr)
339        sec = self.section_plus_offset(addr)
340        if sym or sec:
341            ret += line[:pre]
342            ret += addr
343            if sym:
344                ret += ' ' + sym
345            if sec:
346                ret += ' ' + sec
347            ret += line[post:]
348        return ret
349
350    # Return all ELF sections with the ALLOC flag
351    def read_sections(self, elf_name):
352        if elf_name is None:
353            return
354        if elf_name in self._sections:
355            return
356        elf = self.get_elf(elf_name)
357        if not elf:
358            return
359        cmd = self.arch_prefix('objdump', elf)
360        if not elf or not cmd:
361            return
362        self._sections[elf_name] = []
363        objdump = self.my_Popen([cmd, '--section-headers', elf])
364        for line in iter(objdump.stdout.readline, ''):
365            try:
366                _, name, size, vma, _, _, _ = line.split()
367            except ValueError:
368                if 'ALLOC' in line:
369                    self._sections[elf_name].append([name, int(vma, 16),
370                                                     int(size, 16)])
371
372    def first_vma(self, elf_name):
373        self.read_sections(elf_name)
374        return '0x{:x}'.format(self._sections[elf_name][0][1])
375
376    def overlaps(self, section, addr, size):
377        sec_addr = section[1]
378        sec_size = section[2]
379        if not size or not sec_size:
380            return False
381        return ((addr <= (sec_addr + sec_size - 1)) and
382                ((addr + size - 1) >= sec_addr))
383
384    def sections_in_region(self, addr, size, elf_idx):
385        ret = ''
386        addr = self.subtract_load_addr(addr)
387        if not addr:
388            return ''
389        iaddr = int(addr, 16)
390        isize = int(size, 16)
391        elf = self._elfs[int(elf_idx)][0]
392        if elf is None:
393            return ''
394        self.read_sections(elf)
395        if elf not in self._sections:
396            return ''
397        for s in self._sections[elf]:
398            if self.overlaps(s, iaddr, isize):
399                ret += ' ' + s[0]
400        return ret
401
402    def reset(self):
403        self._call_stack_found = False
404        if self._addr2line:
405            self._addr2line.terminate()
406            self._addr2line = None
407        self._addr2line_elf_name = None
408        self._arch = None
409        self._saved_abort_line = ''
410        self._sections = {}  # {elf_name: [[name, addr, size], ...], ...}
411        self._regions = []   # [[addr, size, elf_idx, saved line], ...]
412        self._elfs = {0: ["tee.elf", 0]}  # {idx: [uuid, load_addr], ...}
413        self._tee_load_addr = '0x0'
414        self._func_graph_found = False
415        self._func_graph_skip_line = True
416
417    def pretty_print_path(self, path):
418        if self._strip_path:
419            return re.sub(re.escape(self._strip_path) + '/*', '', path)
420        return path
421
422    def write(self, line):
423        if self._call_stack_found:
424            match = re.search(STACK_ADDR_RE, line)
425            if match:
426                addr = match.group('addr')
427                pre = match.start('addr')
428                post = match.end('addr')
429                self._out.write(line[:pre])
430                self._out.write(addr)
431                # The call stack contains return addresses (LR/ELR values).
432                # Heuristic: subtract 2 to obtain the call site of the function
433                # or the location of the exception. This value works for A64,
434                # A32 as well as Thumb.
435                pc = 0
436                lr = int(addr, 16)
437                if lr:
438                    pc = lr - 2
439                res = self.resolve('0x{:x}'.format(pc))
440                res = self.pretty_print_path(res)
441                self._out.write(' ' + res)
442                self._out.write(line[post:])
443                return
444            else:
445                self.reset()
446        if self._func_graph_found:
447            match = re.search(GRAPH_ADDR_RE, line)
448            match_re = re.search(GRAPH_RE, line)
449            if match:
450                addr = match.group('addr')
451                pre = match.start('addr')
452                post = match.end('addr')
453                self._out.write(line[:pre])
454                res = self.resolve(addr)
455                res_arr = re.split(' ', res)
456                self._out.write(res_arr[0])
457                self._out.write(line[post:])
458                self._func_graph_skip_line = False
459                return
460            elif match_re:
461                self._out.write(line)
462                return
463            elif self._func_graph_skip_line:
464                return
465            else:
466                self.reset()
467        match = re.search(REGION_RE, line)
468        if match:
469            # Region table: save info for later processing once
470            # we know which UUID corresponds to which ELF index
471            addr = match.group('addr')
472            size = match.group('size')
473            elf_idx = match.group('elf_idx')
474            self._regions.append([addr, size, elf_idx, line])
475            return
476        match = re.search(ELF_LIST_RE, line)
477        if match:
478            # ELF list: save info for later. Region table and ELF list
479            # will be displayed when the call stack is reached
480            i = int(match.group('idx'))
481            self._elfs[i] = [match.group('uuid'), match.group('load_addr'),
482                             line]
483            return
484        match = re.search(TA_PANIC_RE, line)
485        if match:
486            code = match.group('code')
487            if code in tee_result_names:
488                line = line.strip() + ' (' + tee_result_names[code] + ')\n'
489            self._out.write(line)
490            return
491        match = re.search(TEE_LOAD_ADDR_RE, line)
492        if match:
493            self._tee_load_addr = match.group('load_addr')
494        match = re.search(CALL_STACK_RE, line)
495        if match:
496            self._call_stack_found = True
497            if self._regions:
498                for r in self._regions:
499                    r_addr = r[0]
500                    r_size = r[1]
501                    elf_idx = r[2]
502                    saved_line = r[3]
503                    if elf_idx is None:
504                        self._out.write(saved_line)
505                    else:
506                        self._out.write(saved_line.strip() +
507                                        self.sections_in_region(r_addr,
508                                                                r_size,
509                                                                elf_idx) +
510                                        '\n')
511            if self._elfs:
512                for k in self._elfs:
513                    e = self._elfs[k]
514                    if (len(e) >= 3):
515                        # TA executable or library
516                        self._out.write(e[2].strip())
517                        elf = self.get_elf(e[0])
518                        if elf:
519                            rpath = os.path.realpath(elf)
520                            path = self.pretty_print_path(rpath)
521                            self._out.write(' (' + path + ')')
522                        self._out.write('\n')
523            # Here is a good place to resolve the abort address because we
524            # have all the information we need
525            if self._saved_abort_line:
526                self._out.write(self.process_abort(self._saved_abort_line))
527        match = re.search(FUNC_GRAPH_RE, line)
528        if match:
529            self._func_graph_found = True
530        match = re.search(ABORT_ADDR_RE, line)
531        if match:
532            self.reset()
533            # At this point the arch and TA load address are unknown.
534            # Save the line so We can translate the abort address later.
535            self._saved_abort_line = line
536        self._out.write(line)
537
538    def flush(self):
539        self._out.flush()
540
541
542def main():
543    args = get_args()
544    if args.dir:
545        # Flatten list in case -d is used several times *and* with multiple
546        # arguments
547        args.dirs = [item for sublist in args.dir for item in sublist]
548    else:
549        args.dirs = []
550    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
551
552    fd = sys.stdin.fileno()
553    isatty = os.isatty(fd)
554    if isatty:
555        old = termios.tcgetattr(fd)
556        new = termios.tcgetattr(fd)
557        new[3] = new[3] & ~termios.ECHO  # lflags
558    try:
559        if isatty:
560            termios.tcsetattr(fd, termios.TCSADRAIN, new)
561        for line in sys.stdin:
562            symbolizer.write(line)
563    finally:
564        symbolizer.flush()
565        if isatty:
566            termios.tcsetattr(fd, termios.TCSADRAIN, old)
567
568
569if __name__ == "__main__":
570    main()
571