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 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 'Seccomp', 42) 43PsOutput = namedtuple("PsOutput", 44 ' '.join([field.split(':')[0].lower() 45 for field in PS_FIELDS + STATUS_FIELDS])) 46 47# Constants that match the values in /proc/PID/status Seccomp field. 48# See `man 5 proc` for more details. 49SECCOMP_MODE_DISABLED = '0' 50SECCOMP_MODE_STRICT = '1' 51SECCOMP_MODE_FILTER = '2' 52# For human readable strings. 53SECCOMP_MAP = { 54 SECCOMP_MODE_DISABLED: 'disabled', 55 SECCOMP_MODE_STRICT: 'strict', 56 SECCOMP_MODE_FILTER: 'filter', 57} 58 59 60def get_properties(service, init_process): 61 """Returns a dictionary of the properties of a service. 62 63 @param service: the PsOutput of the service. 64 @param init_process: the PsOutput of the init process. 65 """ 66 67 properties = dict(service._asdict()) 68 properties['exe'] = service.comm 69 properties['pidns'] = yes_or_no(service.pidns != init_process.pidns) 70 properties['caps'] = yes_or_no(service.capeff != init_process.capeff) 71 properties['filter'] = yes_or_no(service.seccomp == SECCOMP_MODE_FILTER) 72 return properties 73 74 75def yes_or_no(value): 76 """Returns 'Yes' or 'No' based on the truthiness of a value.""" 77 return 'Yes' if value else 'No' 78 79 80class security_SandboxedServices(test.test): 81 """Enforces sandboxing restrictions on the processes running 82 on the system. 83 """ 84 85 version = 1 86 87 88 def get_running_processes(self): 89 """Returns a list of running processes as PsOutput objects.""" 90 91 usermax = utils.system_output("cut -d: -f1 /etc/passwd | wc -L", 92 ignore_status=True) 93 groupmax = utils.system_output('cut -d: -f1 /etc/group | wc -L', 94 ignore_status=True) 95 # Even if the names are all short, make sure we have enough space 96 # to hold numeric 32-bit ids too (can come up with userns). 97 usermax = max(int(usermax), 10) 98 groupmax = max(int(groupmax), 10) 99 fields = { 100 'usermax': usermax, 101 'groupmax': groupmax, 102 } 103 ps_cmd = ('ps --no-headers -ww -eo ' + 104 (','.join(PS_FIELDS) % fields)) 105 ps_fields_len = len(PS_FIELDS) 106 107 output = utils.system_output(ps_cmd) 108 logging.debug('output of ps:\n%s', output) 109 110 # Fill in fields that `ps` doesn't support but are in /proc/PID/status. 111 # Example line output: 112 # Pid:1 CapInh:0000000000000000 CapPrm:0000001fffffffff CapEff:0000001fffffffff CapBnd:0000001fffffffff Seccomp:0 113 cmd = ( 114 "awk '$1 ~ \"^(Pid|%s):\" " 115 "{printf \"%%s%%s \", $1, $NF; if ($1 == \"%s:\") printf \"\\n\"}'" 116 " /proc/[1-9]*/status" 117 ) % ('|'.join(STATUS_FIELDS), STATUS_FIELDS[-1]) 118 # Processes might exit while awk is running, so ignore its exit status. 119 status_output = utils.system_output(cmd, ignore_status=True) 120 # Turn each line into a dict. 121 # [ 122 # {'pid': '1', 'CapInh': '0000000000000000', 'Seccomp': '0', ...}, 123 # {'pid': '10', ...}, 124 # ..., 125 # ] 126 status_list = list(dict(attr.split(':', 1) for attr in line.split()) 127 for line in status_output.splitlines()) 128 # Create a dict mapping a pid to its extended status data. 129 # { 130 # '1': {'pid': '1', 'CapInh': '0000000000000000', ...}, 131 # '2': {'pid': '2', ...}, 132 # ..., 133 # } 134 status_data = dict((x['Pid'], x) for x in status_list) 135 logging.debug('output of awk:\n%s', status_output) 136 137 # Now merge the two sets of process data. 138 running_processes = [] 139 for line in output.splitlines(): 140 # crbug.com/422700: Filter out zombie processes. 141 if '<defunct>' in line: 142 continue 143 144 fields = line.split(None, ps_fields_len - 1) 145 pid = fields[0] 146 # The process lists might not be exactly the same (since we gathered 147 # data with multiple commands), and not all fields might exist (e.g. 148 # older kernels might not have all the fields). 149 pid_data = status_data.get(pid, {}) 150 status_fields = [pid_data.get(key) for key in STATUS_FIELDS] 151 running_processes.append(PsOutput(*fields + status_fields)) 152 153 return running_processes 154 155 156 def load_baseline(self): 157 """The baseline file lists the services we know and 158 whether (and how) they are sandboxed. 159 """ 160 161 def load(path): 162 """Load baseline from |path| and return its fields and dictionary. 163 164 @param path: The baseline to load. 165 """ 166 logging.info('Loading baseline %s', path) 167 reader = csv.DictReader(open(path)) 168 return reader.fieldnames, dict((d['exe'], d) for d in reader 169 if not d['exe'].startswith('#')) 170 171 baseline_path = os.path.join(self.bindir, 'baseline') 172 fields, ret = load(baseline_path) 173 174 board = utils.get_current_board() 175 baseline_path += '.' + board 176 if os.path.exists(baseline_path): 177 new_fields, new_entries = load(baseline_path) 178 if new_fields != fields: 179 raise error.TestError('header mismatch in %s' % baseline_path) 180 ret.update(new_entries) 181 182 return fields, ret 183 184 185 def load_exclusions(self): 186 """The exclusions file lists running programs 187 that we don't care about (for now). 188 """ 189 190 exclusions_path = os.path.join(self.bindir, 'exclude') 191 return set(line.strip() for line in open(exclusions_path) 192 if not line.startswith('#')) 193 194 195 def dump_services(self, fieldnames, running_services_properties): 196 """Leaves a list of running services in the results dir 197 so that we can update the baseline file if necessary. 198 199 @param fieldnames: list of fields to be written. 200 @param running_services_properties: list of services to be logged. 201 """ 202 203 file_path = os.path.join(self.resultsdir, 'running_services') 204 with open(file_path, 'w') as output_file: 205 writer = csv.DictWriter(output_file, fieldnames=fieldnames, 206 extrasaction='ignore') 207 writer.writeheader() 208 for service_properties in running_services_properties: 209 writer.writerow(service_properties) 210 211 212 def run_once(self): 213 """Inspects the process list, looking for root and sandboxed processes 214 (with some exclusions). If we have a baseline entry for a given process, 215 confirms it's an exact match. Warns if we see root or sandboxed 216 processes that we have no baseline for, and warns if we have 217 baselines for processes not seen running. 218 """ 219 220 fieldnames, baseline = self.load_baseline() 221 exclusions = self.load_exclusions() 222 running_processes = self.get_running_processes() 223 is_asan = asan.running_on_asan() 224 if is_asan: 225 logging.info('ASAN image detected -> skipping seccomp checks') 226 227 kthreadd_pid = -1 228 229 init_process = None 230 running_services = {} 231 232 # Filter running processes list 233 for process in running_processes: 234 exe = process.comm 235 236 if exe == "kthreadd": 237 kthreadd_pid = process.pid 238 continue 239 elif process.pid == "1": 240 init_process = process 241 continue 242 243 # Don't worry about kernel threads 244 if process.ppid == kthreadd_pid: 245 continue 246 247 if exe in exclusions: 248 continue 249 250 running_services[exe] = process 251 252 if not init_process: 253 raise error.TestFail("Cannot find init process") 254 255 # Find differences between running services and baseline 256 services_set = set(running_services.keys()) 257 baseline_set = set(baseline.keys()) 258 259 new_services = services_set.difference(baseline_set) 260 stale_baselines = baseline_set.difference(services_set) 261 262 # Check baseline 263 sandbox_delta = [] 264 for exe in services_set.intersection(baseline_set): 265 process = running_services[exe] 266 267 # If the process is not running as the correct user 268 if process.euser != baseline[exe]["euser"]: 269 logging.error('%s: bad user: wanted "%s" but got "%s"', 270 exe, baseline[exe]['euser'], process.euser) 271 sandbox_delta.append(exe) 272 continue 273 274 # If the process is not running as the correct group 275 if process.egroup != baseline[exe]['egroup']: 276 logging.error('%s: bad group: wanted "%s" but got "%s"', 277 exe, baseline[exe]['egroup'], process.egroup) 278 sandbox_delta.append(exe) 279 continue 280 281 # Check the various sandbox settings. 282 if (baseline[exe]['pidns'] == 'Yes' and 283 process.pidns == init_process.pidns): 284 logging.error('%s: missing pid ns usage', exe) 285 sandbox_delta.append(exe) 286 elif (baseline[exe]['caps'] == 'Yes' and 287 process.capeff == init_process.capeff): 288 logging.error('%s: missing caps usage', exe) 289 sandbox_delta.append(exe) 290 elif (baseline[exe]['filter'] == 'Yes' and 291 process.seccomp != SECCOMP_MODE_FILTER and 292 not is_asan): 293 # Since minijail disables seccomp at runtime when ASAN is 294 # active, we can't enforce it on ASAN bots. Just ignore 295 # the test entirely. (Comment applies to "is_asan" above.) 296 logging.error('%s: missing seccomp usage: wanted %s (%s) but ' 297 'got %s (%s)', exe, SECCOMP_MODE_FILTER, 298 SECCOMP_MAP[SECCOMP_MODE_FILTER], process.seccomp, 299 SECCOMP_MAP.get(process.seccomp, '???')) 300 sandbox_delta.append(exe) 301 302 # Save current run to results dir 303 running_services_properties = [get_properties(s, init_process) 304 for s in running_services.values()] 305 self.dump_services(fieldnames, running_services_properties) 306 307 if len(stale_baselines) > 0: 308 logging.warn('Stale baselines: %r', stale_baselines) 309 310 if len(new_services) > 0: 311 logging.warn('New services: %r', new_services) 312 313 # We won't complain about new non-root services (on the assumption 314 # that they've already somewhat sandboxed things), but we'll fail 315 # with new root services (on the assumption they haven't done any 316 # sandboxing work). If they really need to run as root, they can 317 # update the baseline to whitelist it. 318 new_root_services = [x for x in new_services 319 if running_services[x].euser == 'root'] 320 if new_root_services: 321 logging.error('New services are not allowed to run as root, ' 322 'but these are: %r', new_root_services) 323 sandbox_delta.extend(new_root_services) 324 325 if len(sandbox_delta) > 0: 326 logging.error('Failed sandboxing: %r', sandbox_delta) 327 raise error.TestFail("One or more processes failed sandboxing") 328