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 5import csv 6import logging 7import os 8 9from collections import namedtuple, defaultdict 10 11from autotest_lib.client.bin import test 12from autotest_lib.client.common_lib import error 13from autotest_lib.client.common_lib import utils 14from autotest_lib.client.cros import asan 15 16 17PS_FIELDS = ( 18 'pid', 19 'ppid', 20 'comm:32', 21 'euser:%(usermax)d', 22 'ruser:%(usermax)d', 23 'egroup:%(groupmax)d', 24 'rgroup:%(groupmax)d', 25 'ipcns', 26 'mntns', 27 'netns', 28 'pidns', 29 'userns', 30 'utsns', 31 'args', 32) 33# These fields aren't available via ps, so we have to get them indirectly. 34# Note: Case is significant as the fields match the /proc/PID/status file. 35STATUS_FIELDS = ( 36 'CapInh', 37 'CapPrm', 38 'CapEff', 39 'CapBnd', 40 'CapAmb', 41 'NoNewPrivs', 42 'Seccomp', 43) 44# These fields are not available via ps or /proc/PID/status. 45EXTRA_FIELDS = ( 46 'mountinfo', 47) 48PsOutput = namedtuple("PsOutput", 49 ' '.join([field.split(':')[0].lower() for field in 50 PS_FIELDS + STATUS_FIELDS + EXTRA_FIELDS])) 51 52# Constants that match the values in /proc/PID/status Seccomp field. 53# See `man 5 proc` for more details. 54SECCOMP_MODE_DISABLED = '0' 55SECCOMP_MODE_STRICT = '1' 56SECCOMP_MODE_FILTER = '2' 57# For human readable strings. 58SECCOMP_MAP = { 59 SECCOMP_MODE_DISABLED: 'disabled', 60 SECCOMP_MODE_STRICT: 'strict', 61 SECCOMP_MODE_FILTER: 'filter', 62} 63 64# These mounts only occur in test images. They should be limited to the init 65# mount namespace, so no service should have them. 66TEST_IMAGE_MOUNTS = ( 67 '/usr/local', 68 '/var/db/pkg', 69 '/var/lib/portage', 70) 71 72 73def get_properties(service, init_process): 74 """Returns a dictionary of the properties of a service. 75 76 @param service: the PsOutput of the service. 77 @param init_process: the PsOutput of the init process. 78 """ 79 80 properties = dict(service._asdict()) 81 properties['exe'] = service.comm 82 properties['pidns'] = yes_or_no(service.pidns != init_process.pidns) 83 properties['mntns'] = yes_or_no(service.mntns != init_process.mntns) 84 properties['caps'] = yes_or_no(service.capeff != init_process.capeff) 85 properties['nonewprivs'] = yes_or_no(service.nonewprivs == '1') 86 properties['filter'] = yes_or_no(service.seccomp == SECCOMP_MODE_FILTER) 87 return properties 88 89 90def yes_or_no(value): 91 """Returns 'Yes' or 'No' based on the truthiness of a value. 92 93 @param value: boolean value. 94 """ 95 96 return 'Yes' if value else 'No' 97 98 99def get_mount_info(pid): 100 """Returns the contents of /proc/PID/mountinfo. 101 102 @param pid: The process id. 103 """ 104 try: 105 return tuple(utils.get_mount_info(process=pid)) 106 except IOError as e: 107 # This process might have died already. 108 logging.warning('Failed to read mountinfo for pid %s: %s', pid, e) 109 return () 110 111 112def has_test_image_mounts(mountinfo): 113 """Returns whether a process has test image mounts in its mount namespace. 114 115 @param mountinfo: A list of utils.MountInfo. 116 """ 117 return any(m.mount_point in TEST_IMAGE_MOUNTS for m in mountinfo) 118 119 120class security_SandboxedServices(test.test): 121 """Enforces sandboxing restrictions on the processes running 122 on the system. 123 """ 124 125 version = 1 126 127 128 def get_running_processes(self): 129 """Returns a list of running processes as PsOutput objects.""" 130 131 usermax = utils.system_output("cut -d: -f1 /etc/passwd | wc -L", 132 ignore_status=True) 133 groupmax = utils.system_output('cut -d: -f1 /etc/group | wc -L', 134 ignore_status=True) 135 # Even if the names are all short, make sure we have enough space 136 # to hold numeric 32-bit ids too (can come up with userns). 137 usermax = max(int(usermax), 10) 138 groupmax = max(int(groupmax), 10) 139 fields = { 140 'usermax': usermax, 141 'groupmax': groupmax, 142 } 143 ps_cmd = ('ps --no-headers -ww -eo ' + 144 (','.join(PS_FIELDS) % fields)) 145 ps_fields_len = len(PS_FIELDS) 146 147 output = utils.system_output(ps_cmd) 148 logging.debug('output of ps:\n%s', output) 149 150 # Fill in fields that `ps` doesn't support but are in /proc/PID/status. 151 # Example line output: 152 # Pid:1 CapInh:0000000000000000 CapPrm:0000001fffffffff CapEff:0000001fffffffff CapBnd:0000001fffffffff Seccomp:0 153 cmd = ( 154 "for f in /proc/[1-9]*/status ; do awk '$1 ~ \"^(Pid|%s):\" " 155 "{printf \"%%s%%s \", $1, $NF; if ($1 == \"%s:\") printf \"\\n\"}'" 156 " $f ; done" 157 ) % ('|'.join(STATUS_FIELDS), STATUS_FIELDS[-1]) 158 # Processes might exit while awk is running, so ignore its exit status. 159 status_output = utils.system_output(cmd, ignore_status=True) 160 # Turn each line into a dict. 161 # [ 162 # {'pid': '1', 'CapInh': '0000000000000000', 'Seccomp': '0', ...}, 163 # {'pid': '10', ...}, 164 # ..., 165 # ] 166 status_list = list(dict(attr.split(':', 1) for attr in line.split()) 167 for line in status_output.splitlines()) 168 # Create a dict mapping a pid to its extended status data. 169 # { 170 # '1': {'pid': '1', 'CapInh': '0000000000000000', ...}, 171 # '2': {'pid': '2', ...}, 172 # ..., 173 # } 174 status_data = dict((x['Pid'], x) for x in status_list) 175 logging.debug('output of awk:\n%s', status_output) 176 177 # Now merge the two sets of process data. 178 running_processes = [] 179 for line in output.splitlines(): 180 # crbug.com/422700: Filter out zombie processes. 181 if '<defunct>' in line: 182 continue 183 184 fields = line.split(None, ps_fields_len - 1) 185 pid = fields[0] 186 # The process lists might not be exactly the same (since we gathered 187 # data with multiple commands), and not all fields might exist (e.g. 188 # older kernels might not have all the fields). 189 pid_data = status_data.get(pid, {}) 190 status_fields = [pid_data.get(key) for key in STATUS_FIELDS] 191 extra_fields = [get_mount_info(pid)] 192 running_processes.append( 193 PsOutput(*fields + status_fields + extra_fields)) 194 195 return running_processes 196 197 198 def load_baseline(self): 199 """The baseline file lists the services we know and 200 whether (and how) they are sandboxed. 201 """ 202 203 def load(path): 204 """Load baseline from |path| and return its fields and dictionary. 205 206 @param path: The baseline to load. 207 """ 208 logging.info('Loading baseline %s', path) 209 reader = csv.DictReader(open(path)) 210 return reader.fieldnames, dict((d['exe'], d) for d in reader 211 if not d['exe'].startswith('#')) 212 213 baseline_path = os.path.join(self.bindir, 'baseline') 214 fields, ret = load(baseline_path) 215 216 board = utils.get_current_board() 217 baseline_path += '.' + board 218 if os.path.exists(baseline_path): 219 new_fields, new_entries = load(baseline_path) 220 if new_fields != fields: 221 raise error.TestError('header mismatch in %s' % baseline_path) 222 ret.update(new_entries) 223 224 return fields, ret 225 226 227 def load_exclusions(self): 228 """The exclusions file lists running programs 229 that we don't care about (for now). 230 """ 231 232 exclusions_path = os.path.join(self.bindir, 'exclude') 233 return set(line.strip() for line in open(exclusions_path) 234 if not line.startswith('#')) 235 236 237 def dump_services(self, fieldnames, running_services_properties): 238 """Leaves a list of running services in the results dir 239 so that we can update the baseline file if necessary. 240 241 @param fieldnames: list of fields to be written. 242 @param running_services_properties: list of services to be logged. 243 """ 244 245 file_path = os.path.join(self.resultsdir, 'running_services') 246 with open(file_path, 'w') as output_file: 247 writer = csv.DictWriter(output_file, fieldnames=fieldnames, 248 extrasaction='ignore') 249 writer.writeheader() 250 for service_properties in running_services_properties: 251 writer.writerow(service_properties) 252 253 254 def run_once(self): 255 """Inspects the process list, looking for root and sandboxed processes 256 (with some exclusions). If we have a baseline entry for a given process, 257 confirms it's an exact match. Warns if we see root or sandboxed 258 processes that we have no baseline for, and warns if we have 259 baselines for processes not seen running. 260 """ 261 262 fieldnames, baseline = self.load_baseline() 263 exclusions = self.load_exclusions() 264 running_processes = self.get_running_processes() 265 is_asan = asan.running_on_asan() 266 if is_asan: 267 logging.info('ASAN image detected -> skipping seccomp checks') 268 269 kthreadd_pid = -1 270 271 init_process = None 272 running_services = {} 273 274 # Filter running processes list. 275 for process in running_processes: 276 exe = process.comm 277 278 if exe == "kthreadd": 279 kthreadd_pid = process.pid 280 continue 281 elif process.pid == "1": 282 init_process = process 283 continue 284 285 # Don't worry about kernel threads. 286 if process.ppid == kthreadd_pid: 287 continue 288 289 if exe in exclusions: 290 continue 291 292 running_services[exe] = process 293 294 if not init_process: 295 raise error.TestFail("Cannot find init process") 296 297 # Find differences between running services and baseline. 298 services_set = set(running_services.keys()) 299 baseline_set = set(baseline.keys()) 300 301 new_services = services_set.difference(baseline_set) 302 stale_baselines = defaultdict(list) 303 304 for exe in baseline_set.difference(services_set): 305 stale_baselines[exe].append('unused') 306 307 # Check baseline. 308 sandbox_delta = defaultdict(list) 309 for exe in services_set.intersection(baseline_set): 310 process = running_services[exe] 311 stale_flags = [] 312 errors = [] 313 314 # If the process is not running as the correct user. 315 if process.euser != baseline[exe]["euser"]: 316 errors.append('bad user: wanted "%s" but got "%s"' % 317 (baseline[exe]['euser'], process.euser)) 318 319 # If the process is not running as the correct group. 320 if process.egroup != baseline[exe]['egroup']: 321 errors.append('bad group: wanted "%s" but got "%s"' % 322 (baseline[exe]['egroup'], process.egroup)) 323 324 # Check the various sandbox settings. 325 if process.pidns == init_process.pidns: 326 if baseline[exe]['pidns'] == 'Yes': 327 errors.append('missing pid ns usage') 328 elif baseline[exe]['pidns'] != 'Yes': 329 stale_flags.append('pidns') 330 331 if process.mntns == init_process.mntns: 332 if baseline[exe]['mntns'] == 'Yes': 333 errors.append('missing mount ns usage') 334 elif has_test_image_mounts(process.mountinfo): 335 if baseline[exe]['mntns'] == 'Yes': 336 errors.append('did not call pivot_root(2)') 337 elif baseline[exe]['mntns'] != 'Yes': 338 stale_flags.append('mntns') 339 340 if process.capeff == init_process.capeff: 341 if baseline[exe]['caps'] == 'Yes': 342 errors.append('missing caps usage') 343 elif baseline[exe]['caps'] != 'Yes': 344 stale_flags.append('caps') 345 346 if process.nonewprivs != '1': 347 if baseline[exe]['nonewprivs'] == 'Yes': 348 errors.append('missing NoNewPrivs') 349 elif baseline[exe]['nonewprivs'] != 'Yes': 350 stale_flags.append('nonewprivs') 351 352 if not is_asan: 353 # Since Minijail disables seccomp at runtime when ASAN is 354 # active, we can't enforce it on ASAN bots. Just ignore 355 # the test entirely. (Comment applies to "is_asan" above.) 356 if process.seccomp != SECCOMP_MODE_FILTER: 357 if baseline[exe]['filter'] == 'Yes': 358 errors.append( 359 'missing seccomp usage: ' 360 'wanted %s (%s) but got %s (%s)' % 361 (SECCOMP_MODE_FILTER, 362 SECCOMP_MAP[SECCOMP_MODE_FILTER], process.seccomp, 363 SECCOMP_MAP.get(process.seccomp, '???'))) 364 elif baseline[exe]['filter'] != 'Yes': 365 stale_flags.append('filter') 366 367 if stale_flags: 368 stale_baselines[exe].append('potentially missing flags: %s' % 369 ','.join(stale_flags)) 370 if errors: 371 sandbox_delta[exe].extend(errors) 372 373 # Save current run to results dir. 374 running_services_properties = [get_properties(s, init_process) 375 for s in running_services.values()] 376 self.dump_services(fieldnames, running_services_properties) 377 378 if len(stale_baselines) > 0: 379 logging.warn('Stale baselines: %r', stale_baselines) 380 381 if len(new_services) > 0: 382 logging.warn('New services: %r', new_services) 383 384 # We won't complain about new non-root services (on the assumption 385 # that they've already somewhat sandboxed things), but we'll fail 386 # with new root services (on the assumption they haven't done any 387 # sandboxing work). If they really need to run as root, they can 388 # update the baseline to whitelist it. 389 for exe in new_services: 390 if running_services[exe].euser == 'root': 391 sandbox_delta[exe].append('missing euser') 392 393 if len(sandbox_delta) > 0: 394 for delta_entry in sandbox_delta: 395 logging.error('Failed sandboxing: %s', delta_entry) 396 raise error.TestFail('One or more processes failed sandboxing: %r' % 397 sandbox_delta) 398