1#!/usr/bin/python3
2# Verify that certain symbols are covered by RELRO.
3# Copyright (C) 2022 Free Software Foundation, Inc.
4# This file is part of the GNU C Library.
5#
6# The GNU C Library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# The GNU C Library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with the GNU C Library; if not, see
18# <https://www.gnu.org/licenses/>.
19
20"""Analyze a (shared) object to verify that certain symbols are
21present and covered by the PT_GNU_RELRO segment.
22
23"""
24
25import argparse
26import os.path
27import sys
28
29# Make available glibc Python modules.
30sys.path.append(os.path.join(
31    os.path.dirname(os.path.realpath(__file__)), os.path.pardir, 'scripts'))
32
33import glibcelf
34
35def find_relro(path: str, img: glibcelf.Image) -> (int, int):
36    """Discover the address range of the PT_GNU_RELRO segment."""
37    for phdr in img.phdrs():
38        if phdr.p_type == glibcelf.Pt.PT_GNU_RELRO:
39            # The computation is not entirely accurate because
40            # _dl_protect_relro in elf/dl-reloc.c rounds both the
41            # start end and downwards using the run-time page size.
42            return phdr.p_vaddr, phdr.p_vaddr + phdr.p_memsz
43    sys.stdout.write('{}: error: no PT_GNU_RELRO segment\n'.format(path))
44    sys.exit(1)
45
46def check_in_relro(kind, relro_begin, relro_end, name, start, size, error):
47    """Check if a section or symbol falls within in the RELRO segment."""
48    end = start + size - 1
49    if not (relro_begin <= start < end < relro_end):
50        error(
51            '{} {!r} of size {} at 0x{:x} is not in RELRO range [0x{:x}, 0x{:x})'.format(
52                kind, name.decode('UTF-8'), start, size,
53                relro_begin, relro_end))
54
55def get_parser():
56    """Return an argument parser for this script."""
57    parser = argparse.ArgumentParser(description=__doc__)
58    parser.add_argument('object', help='path to object file to check')
59    parser.add_argument('--required', metavar='NAME', default=(),
60                        help='required symbol names', nargs='*')
61    parser.add_argument('--optional', metavar='NAME', default=(),
62                        help='required symbol names', nargs='*')
63    return parser
64
65def main(argv):
66    """The main entry point."""
67    parser = get_parser()
68    opts = parser.parse_args(argv)
69    img = glibcelf.Image.readfile(opts.object)
70
71    required_symbols = frozenset([sym.encode('UTF-8')
72                                  for sym in opts.required])
73    optional_symbols = frozenset([sym.encode('UTF-8')
74                                  for sym in opts.optional])
75    check_symbols = required_symbols | optional_symbols
76
77    # Tracks the symbols in check_symbols that have been found.
78    symbols_found = set()
79
80    # Discover the extent of the RELRO segment.
81    relro_begin, relro_end = find_relro(opts.object, img)
82    symbol_table_found = False
83
84    errors = False
85    def error(msg: str) -> None:
86        """Record an error condition and write a message to standard output."""
87        nonlocal errors
88        errors = True
89        sys.stdout.write('{}: error: {}\n'.format(opts.object, msg))
90
91    # Iterate over section headers to find the symbol table.
92    for shdr in img.shdrs():
93        if shdr.sh_type == glibcelf.Sht.SHT_SYMTAB:
94            symbol_table_found = True
95            for sym in img.syms(shdr):
96                if sym.st_name in check_symbols:
97                    symbols_found.add(sym.st_name)
98
99                    # Validate symbol type, section, and size.
100                    if sym.st_info.type != glibcelf.Stt.STT_OBJECT:
101                        error('symbol {!r} has wrong type {}'.format(
102                            sym.st_name.decode('UTF-8'), sym.st_info.type))
103                    if sym.st_shndx in glibcelf.Shn:
104                        error('symbol {!r} has reserved section {}'.format(
105                            sym.st_name.decode('UTF-8'), sym.st_shndx))
106                        continue
107                    if sym.st_size == 0:
108                        error('symbol {!r} has size zero'.format(
109                            sym.st_name.decode('UTF-8')))
110                        continue
111
112                    check_in_relro('symbol', relro_begin, relro_end,
113                                   sym.st_name, sym.st_value, sym.st_size,
114                                   error)
115            continue # SHT_SYMTAB
116        if shdr.sh_name == b'.data.rel.ro' \
117           or shdr.sh_name.startswith(b'.data.rel.ro.'):
118            check_in_relro('section', relro_begin, relro_end,
119                           shdr.sh_name, shdr.sh_addr, shdr.sh_size,
120                           error)
121            continue
122
123    if required_symbols - symbols_found:
124        for sym in sorted(required_symbols - symbols_found):
125            error('symbol {!r} not found'.format(sym.decode('UTF-8')))
126
127    if errors:
128        sys.exit(1)
129
130    if not symbol_table_found:
131        sys.stdout.write(
132            '{}: warning: no symbol table found (stripped object)\n'.format(
133                opts.object))
134        sys.exit(77)
135
136if __name__ == '__main__':
137    main(sys.argv[1:])
138