1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""A test verifying Address Space Layout Randomization
6
7Uses system calls to get important pids and then gets information about
8the pids in /proc/<pid>/maps. Restarts the tested processes and reads
9information about them again. If ASLR is enabled, memory mappings should
10change.
11"""
12
13from autotest_lib.client.bin import test
14from autotest_lib.client.bin import utils
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.cros import upstart
17
18import logging
19import time
20import pprint
21import re
22
23def _pidsof(exe_name):
24    """Returns the PIDs of processes with the given name as a list."""
25    output = utils.system_output('pidof %s' % exe_name,
26                                 ignore_status=True).strip()
27    return [int(pid) for pid in output.split()]
28
29
30class Process(object):
31    """Holds information about a process.
32
33    Stores basic information about a process. This class is a base for
34    UpstartProcess and SystemdProcess declared below.
35
36    Attributes:
37        _name: String name of process.
38        _service_name: Name of the service corresponding to the process.
39        _parent: String name of process's parent.
40    """
41
42    _START_POLL_INTERVAL_SECONDS = 1
43    _START_TIMEOUT = 30
44
45    def __init__(self, name, service_name, parent):
46        self._name = name
47        self._service_name = service_name
48        self._parent = parent
49
50    def get_name(self):
51        return self._name
52
53    def get_pid(self):
54        """Gets pid of process, waiting for it if not found.
55
56        Raises:
57            error.TestFail: corresponding process is not found.
58        """
59        retries = 0
60        ps_results = ""
61        while retries < self._START_TIMEOUT:
62            # Find all PIDs matching the expected parent name, then find all
63            # PIDs that have the expected process name and any of the parent
64            # PIDs. Only succeed when there is exactly one PID/PPID pairing.
65            # This is needed to handle cases where multiple processes share the
66            # expected parent name. See crbug.com/741110 for background.
67            ppids = _pidsof(self._parent)
68            if len(ppids) > 0:
69                ppid_match = ' '.join('-e " %d$"' % ppid for ppid in ppids)
70                get_pid_command = ('ps -C %s -o pid,ppid | grep %s'
71                    ' | awk \'{print $1}\'') % (self._name, ppid_match)
72                ps_results = utils.system_output(get_pid_command).strip()
73                pids = ps_results.split()
74                if len(pids) == 1:
75                    return pids[0]
76                elif len(pids) > 1:
77                    # More than one candidate process found - rather than pick
78                    # one arbitrarily, continue to wait. This is not expected -
79                    # but continuing to wait will avoid weird failures if some
80                    # time in the future there are multiple non-transient
81                    # parent/child processes with the same names.
82                    logging.debug("Found multiple processes for '%s'",
83                                  self._name)
84
85            # The process, or its parent, could not be found. We then sleep,
86            # hoping the process is just slow to initially start.
87            time.sleep(self._START_POLL_INTERVAL_SECONDS)
88            retries += 1
89
90        # We never saw the process, so abort with details on who was missing.
91        raise error.TestFail('Never saw a pid for "%s"' % (self._name))
92
93
94class UpstartProcess(Process):
95    """Represents an Upstart service."""
96
97    def __init__(self, name, service_name, parent='init'):
98        super(UpstartProcess, self).__init__(name, service_name, parent)
99
100    def exists(self):
101        """Checks if the service is present in Upstart configuration."""
102        return upstart.has_service(self._service_name)
103
104    def restart(self):
105        """Restarts the process via initctl."""
106        utils.system('initctl restart %s' % self._service_name)
107
108class SystemdProcess(Process):
109    """Represents an systemd service."""
110
111    def __init__(self, name, service_name, parent='systemd'):
112        super(SystemdProcess, self).__init__(name, service_name, parent)
113
114    def exists(self):
115        """Checks if the service is present in systemd configuration."""
116        cmd = 'systemctl show -p ActiveState %s.service' % self._service_name
117        output = utils.system_output(cmd, ignore_status=True).strip()
118        return output == 'ActiveState=active'
119
120    def restart(self):
121        """Restarts the process via systemctl."""
122        # Reset the restart rate counter each time before process restart to
123        # avoid systemd restart rate limiting.
124        utils.system('systemctl reset-failed %s' % self._service_name)
125        utils.system('systemctl restart %s' % self._service_name)
126
127class Mapping(object):
128    """Holds information about a process's address mapping.
129
130    Stores information about one memory mapping for a process.
131
132    Attributes:
133        _name: String name of process/memory occupying the location.
134        _start: String containing memory address range start.
135    """
136    def __init__(self, name, start):
137        self._start = start
138        self._name = name
139
140    def set_start(self, new_value):
141        self._start = new_value
142
143    def get_start(self):
144        return self._start
145
146    def __repr__(self):
147        return "<mapping %s %s>" % (self._name, self._start)
148
149
150class security_ASLR(test.test):
151    """Runs ASLR tests
152
153    See top document comments for more information.
154
155    Attributes:
156        version: Current version of the test.
157    """
158    version = 1
159
160    _TEST_ITERATION_COUNT = 5
161
162    _ASAN_SYMBOL = "__asan_init"
163
164    # 'update_engine' should at least be present on all boards.
165    _PROCESS_LIST = [UpstartProcess('chrome', 'ui', parent='session_manager'),
166                     UpstartProcess('debugd', 'debugd'),
167                     UpstartProcess('update_engine', 'update-engine'),
168                     SystemdProcess('update_engine', 'update-engine'),
169                     SystemdProcess('systemd-journald', 'systemd-journald'),]
170
171
172    def get_processes_to_test(self):
173        """Gets processes to test for main function.
174
175        Called by run_once to get processes for this program to test.
176        Filters binaries that actually exist on the system.
177        This has to be a method because it constructs process objects.
178
179        Returns:
180            A list of process objects to be tested (see below for
181            definition of process class).
182        """
183        return [p for p in self._PROCESS_LIST if p.exists()]
184
185
186    def running_on_asan(self):
187        """Returns whether we're running on ASan."""
188        # -q, --quiet         * Only output 'bad' things
189        # -F, --format <arg>  * Use specified format for output
190        # -g, --gmatch        * Use regex rather than string compare (with -s)
191        # -s, --symbol <arg>  * Find a specified symbol
192        scanelf_command = "scanelf -qF'%s#F'"
193        scanelf_command += " -gs %s `which debugd`" % self._ASAN_SYMBOL
194        symbol = utils.system_output(scanelf_command)
195        logging.debug("running_on_asan(): symbol: '%s', _ASAN_SYMBOL: '%s'",
196                      symbol, self._ASAN_SYMBOL)
197        return symbol != ""
198
199
200    def test_randomization(self, process):
201        """Tests ASLR of a single process.
202
203        This is the main test function for the program. It creates data
204        structures out of useful information from sampling /proc/<pid>/maps
205        after restarting the process and then compares address starting
206        locations of all executable, stack, and heap memory from each iteration.
207
208        @param process: a process object representing the process to be tested.
209
210        Returns:
211            A dict containing a Boolean for whether or not the test passed
212            and a list of string messages about passing/failing cases.
213        """
214        test_result = dict([('pass', True), ('results', []), ('cases', dict())])
215        name = process.get_name()
216        mappings = list()
217        pid = -1
218        for i in range(self._TEST_ITERATION_COUNT):
219            new_pid = process.get_pid()
220            if pid == new_pid:
221                raise error.TestFail(
222                    'Service "%s" retained PID %d after restart.' % (name, pid))
223            pid = new_pid
224            mappings.append(self.map(pid))
225            process.restart()
226        logging.debug('Complete mappings dump for process %s:\n%s',
227                      name, pprint.pformat(mappings, 4))
228
229        initial_map = mappings[0]
230        for i, mapping in enumerate(mappings[1:]):
231            logging.debug('Iteration %d', i)
232            for key in mapping.iterkeys():
233                # Set default case result to fail, pass when an address change
234                # occurs.
235                if not test_result['cases'].has_key(key):
236                    test_result['cases'][key] = dict([('pass', False),
237                            ('number', 0),
238                            ('total', self._TEST_ITERATION_COUNT)])
239                was_same = (initial_map.has_key(key) and
240                        initial_map[key].get_start() ==
241                        mapping[key].get_start())
242                if was_same:
243                    logging.debug("Bad: %s address didn't change", key)
244                else:
245                    logging.debug('Good: %s address changed', key)
246                    test_result['cases'][key]['number'] += 1
247                    test_result['cases'][key]['pass'] = True
248        for case, result in test_result['cases'].iteritems():
249            if result['pass']:
250                test_result['results'].append( '[PASS] Address for %s '
251                        'successfully changed' % case)
252            else:
253                test_result['results'].append('[FAIL] Address for %s had '
254                        'deterministic value: %s' % (case,
255                        mappings[0][case].get_start()))
256            test_result['pass'] = test_result['pass'] and result['pass']
257        return test_result
258
259
260    def map(self, pid):
261        """Creates data structure from table in /proc/<pid>/maps.
262
263        Gets all data from /proc/<pid>/maps, parses each entry, and saves
264        entries corresponding to executable, stack, or heap memory into
265        a dictionary.
266
267        @param pid: a string containing the pid to be tested.
268
269        Returns:
270            A dict mapping names to mapping objects (see above for mapping
271            definition).
272        """
273        memory_map = dict()
274        maps_file = open("/proc/%s/maps" % pid)
275        for maps_line in maps_file:
276            result = self.parse_result(maps_line)
277            if result is None:
278                continue
279            name = result['name']
280            start = result['start']
281            perms = result['perms']
282            is_memory = name == '[heap]' or name == '[stack]'
283            is_useful = re.search('x', perms) is not None or is_memory
284            if not is_useful:
285                continue
286            if not name in memory_map:
287                memory_map[name] = Mapping(name, start)
288            elif memory_map[name].get_start() < start:
289                memory_map[name].set_start(start)
290        return memory_map
291
292
293    def parse_result(self, result):
294        """Builds dictionary from columns of a line of /proc/<pid>/maps
295
296        Uses regular expressions to determine column separations. Puts
297        column data into a dict mapping column names to their string values.
298
299        @param result: one line of /proc/<pid>/maps as a string, for any <pid>.
300
301        Returns:
302            None if the regular expression wasn't matched. Otherwise:
303            A dict of string column names mapped to their string values.
304            For example:
305
306        {'start': '9e981700000', 'end': '9e981800000', 'perms': 'rwxp',
307            'something': '00000000', 'major': '00', 'minor': '00', 'inode':
308            '00'}
309        """
310        # Build regex to parse one line of proc maps table.
311        memory = r'(?P<start>\w+)-(?P<end>\w+)'
312        perms = r'(?P<perms>(r|-)(w|-)(x|-)(s|p))'
313        something = r'(?P<something>\w+)'
314        devices = r'(?P<major>\w+):(?P<minor>\w+)'
315        inode = r'(?P<inode>[0-9]+)'
316        name = r'(?P<name>([a-zA-Z0-9/]+|\[heap\]|\[stack\]))'
317        regex = r'%s +%s +%s +%s +%s +%s' % (memory, perms, something,
318            devices, inode, name)
319        found_match = re.match(regex, result)
320        if found_match is None:
321            return None
322        parsed_result = found_match.groupdict()
323        return parsed_result
324
325
326    def run_once(self):
327        """Main function.
328
329        Called when test is run. Gets processes to test and calls test on
330        them.
331
332        Raises:
333            error.TestFail if any processes' memory mapping addresses are the
334            same after restarting.
335        """
336
337        if self.running_on_asan():
338            logging.warning("security_ASLR is not available on ASan.")
339            return
340
341        processes = self.get_processes_to_test()
342        # If we don't find any of the processes we wanted to test, we fail.
343        if len(processes) == 0:
344            proc_names = ", ".join([p.get_name() for p in self._PROCESS_LIST])
345            raise error.TestFail(
346                'Could not find any of "%s" processes to test' % proc_names)
347
348        aslr_enabled = True
349        full_results = dict()
350        for process in processes:
351            test_results = self.test_randomization(process)
352            full_results[process.get_name()] = test_results['results']
353            if not test_results['pass']:
354                aslr_enabled = False
355
356        logging.debug('SUMMARY:')
357        for process_name, results in full_results.iteritems():
358            logging.debug('Results for %s:', process_name)
359            for result in results:
360                logging.debug(result)
361
362        if not aslr_enabled:
363            raise error.TestFail('One or more processes had deterministic '
364                    'memory mappings')
365