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 import csv 6 import getopt 7 import logging 8 import os 9 import re 10 11 from collections import namedtuple 12 13 from autotest_lib.client.bin import test 14 from autotest_lib.client.common_lib import error 15 from autotest_lib.client.common_lib import utils 16 17 18 PS_FIELDS = "pid,ppid,comm:32,euser:%d,ruser:%d,args" 19 PsOutput = namedtuple("PsOutput", 20 ' '.join([field.split(':')[0] 21 for field in PS_FIELDS.split(',')])) 22 23 MINIJAIL_OPTS = { "mj_uid": "-u", 24 "mj_gid": "-g", 25 "mj_pidns": "-p", 26 "mj_caps": "-c", 27 "mj_filter": "-S" } 28 29 30 class security_SandboxedServices(test.test): 31 """Enforces sandboxing restrictions on the processes running 32 on the system. 33 """ 34 35 version = 1 36 37 38 def get_minijail_opts(self): 39 """Parses Minijail's help and generates a getopt string. 40 """ 41 42 help = utils.system_output("minijail0 -h", ignore_status=True) 43 help_lines = help.splitlines()[1:] 44 45 opt_list = [] 46 47 for line in help_lines: 48 # Example lines: 49 # ' -c <caps>: restrict caps to <caps>' 50 # ' -s: use seccomp' 51 m = re.search("-(\w)( <.+>)?:", line) 52 53 if m: 54 opt_list.append(m.groups()[0]) 55 56 if m.groups()[1]: 57 # The option takes an argument 58 opt_list.append(':') 59 60 return ''.join(opt_list) 61 62 63 def get_running_processes(self): 64 """Returns a list of running processes as PsOutput objects.""" 65 66 usermax = utils.system_output("cut -d: -f1 /etc/passwd | wc -L", 67 ignore_status=True) 68 usermax = max(int(usermax), 8) 69 ps_cmd = "ps --no-headers -ww -eo " + (PS_FIELDS % (usermax, usermax)) 70 ps_fields_len = len(PS_FIELDS.split(',')) 71 72 output = utils.system_output(ps_cmd) 73 # crbug.com/422700: Filter out zombie processes. 74 running_processes = [PsOutput(*line.split(None, ps_fields_len - 1)) 75 for line in output.splitlines() 76 if "<defunct>" not in line] 77 return running_processes 78 79 80 def load_baseline(self): 81 """The baseline file lists the services we know and 82 whether (and how) they are sandboxed. 83 """ 84 85 baseline_path = os.path.join(self.bindir, 'baseline') 86 dict_reader = csv.DictReader(open(baseline_path)) 87 return dict([(d["exe"], d) for d in dict_reader]) 88 89 90 def load_exclusions(self): 91 """The exclusions file lists running programs 92 that we don't care about (for now). 93 """ 94 95 exclusions_path = os.path.join(self.bindir, 'exclude') 96 return set([line.strip() for line in open(exclusions_path)]) 97 98 99 def minijail_ok(self, launcher, expected): 100 """Checks whether the Minijail invocation 101 has the correct command-line options. 102 103 @param launcher: Minijail command line for the process. 104 @param expected: Sandboxing restrictions expected. 105 """ 106 107 opts, args = getopt.getopt(launcher.args.split()[1:], 108 self.get_minijail_opts()) 109 optset = set([opt[0] for opt in opts]) 110 111 missing_opts = [] 112 new_opts = [] 113 114 for check, opt in MINIJAIL_OPTS.iteritems(): 115 if expected[check] == "Yes": 116 if opt not in optset: 117 missing_opts.append(check) 118 elif expected[check] == "No": 119 if opt in optset: 120 new_opts.append(check) 121 122 if len(new_opts) > 0: 123 logging.error("New Minijail opts for '%s': %s", 124 expected["exe"], ', '.join(new_opts)) 125 126 if len(missing_opts) > 0: 127 logging.error("Missing Minijail options for '%s': %s", 128 expected["exe"], ', '.join(missing_opts)) 129 130 return (len(new_opts) + len(missing_opts)) == 0 131 132 133 def dump_services(self, running_services, minijail_processes): 134 """Leaves a list of running services in the results dir 135 so that we can update the baseline file if necessary. 136 137 @param running_services: list of services to be logged. 138 @param minijail_processes: list of Minijail processes used to log how 139 each running service is sandboxed. 140 """ 141 142 csv_file = csv.writer(open(os.path.join(self.resultsdir, 143 "running_services"), 'w')) 144 145 for service in running_services: 146 service_minijail = "" 147 148 if service.ppid in minijail_processes: 149 launcher = minijail_processes[service.ppid] 150 service_minijail = launcher.args.split("--")[0].strip() 151 152 row = [service.comm, service.euser, service.args, service_minijail] 153 csv_file.writerow(row) 154 155 156 def log_process_list(self, logger, title, list): 157 report = "%s: %s" % (title, ', '.join(list)) 158 logger(report) 159 160 161 def log_process_list_warn(self, title, list): 162 self.log_process_list(logging.warn, title, list) 163 164 165 def log_process_list_error(self, title, list): 166 self.log_process_list(logging.error, title, list) 167 168 169 def run_once(self): 170 """Inspects the process list, looking for root and sandboxed processes 171 (with some exclusions). If we have a baseline entry for a given process, 172 confirms it's an exact match. Warns if we see root or sandboxed 173 processes that we have no baseline for, and warns if we have 174 baselines for processes not seen running. 175 """ 176 177 baseline = self.load_baseline() 178 exclusions = self.load_exclusions() 179 running_processes = self.get_running_processes() 180 181 kthreadd_pid = -1 182 183 running_services = {} 184 minijail_processes = {} 185 186 # Filter running processes list 187 for process in running_processes: 188 exe = process.comm 189 190 if exe == "kthreadd": 191 kthreadd_pid = process.pid 192 continue 193 194 # Don't worry about kernel threads 195 if process.ppid == kthreadd_pid: 196 continue 197 198 if exe in exclusions: 199 continue 200 201 # Remember minijail0 invocations 202 if exe == "minijail0": 203 minijail_processes[process.pid] = process 204 continue 205 206 running_services[exe] = process 207 208 # Find differences between running services and baseline 209 services_set = set(running_services.keys()) 210 baseline_set = set(baseline.keys()) 211 212 new_services = services_set.difference(baseline_set) 213 stale_baselines = baseline_set.difference(services_set) 214 215 # Check baseline 216 sandbox_delta = [] 217 for exe in services_set.intersection(baseline_set): 218 process = running_services[exe] 219 220 # If the process is not running as the correct user 221 if process.euser != baseline[exe]["euser"]: 222 sandbox_delta.append(exe) 223 continue 224 225 # If this process is supposed to be sandboxed 226 if baseline[exe]["mj_uid"] == "Yes": 227 # If it's not being launched from Minijail, 228 # it's not sandboxed wrt the baseline. 229 if process.ppid not in minijail_processes: 230 sandbox_delta.append(exe) 231 else: 232 launcher = minijail_processes[process.ppid] 233 expected = baseline[exe] 234 if not self.minijail_ok(launcher, expected): 235 sandbox_delta.append(exe) 236 237 # Save current run to results dir 238 self.dump_services(running_services.values(), minijail_processes) 239 240 if len(stale_baselines) > 0: 241 self.log_process_list_warn("Stale baselines", stale_baselines) 242 243 if len(new_services) > 0: 244 self.log_process_list_warn("New services", new_services) 245 246 if len(sandbox_delta) > 0: 247 self.log_process_list_error("Failed sandboxing", sandbox_delta) 248 raise error.TestFail("One or more processes failed sandboxing") 249