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