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