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