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 logging 6 import pwd 7 import re 8 9 from autotest_lib.client.bin import test, utils 10 from autotest_lib.client.common_lib import error 11 12 class security_StatefulPermissions(test.test): 13 """ 14 Report all unexpected writable paths in the /mnt/stateful_partition 15 tree. 16 """ 17 version = 1 18 _STATEFUL_ROOT = "/mnt/stateful_partition" 19 20 # Note that chronos permissions in /home are covered in greater detail 21 # by 'security_ProfilePermissions'. 22 _masks_byuser = {"adm": [], 23 "attestation": ["/unencrypted/preserve/attestation.epb"], 24 "avfs": [], 25 "bin": [], 26 "bluetooth": ["/encrypted/var/lib/bluetooth"], 27 "chaps": ["/encrypted/var/lib/chaps"], 28 "chronos": ["/encrypted/chronos", 29 "/encrypted/var/cache/app_pack", 30 "/encrypted/var/cache/device_local_account_component_policy", 31 "/encrypted/var/cache/device_local_account_extensions", 32 "/encrypted/var/cache/device_local_account_external_policy_data", 33 "/encrypted/var/cache/echo", 34 "/encrypted/var/cache/external_cache", 35 "/encrypted/var/cache/shared_extensions", 36 "/encrypted/var/cache/touch_trial/selection", 37 "/encrypted/var/lib/cromo", 38 "/encrypted/var/lib/opencryptoki", 39 "/encrypted/var/lib/timezone", 40 "/encrypted/var/lib/Synaptics/chronos.1000", 41 "/encrypted/var/log/chrome", 42 "/encrypted/var/log/connectivity.bak", 43 "/encrypted/var/log/connectivity.log", 44 "/encrypted/var/log/metrics", 45 "/encrypted/var/minidumps", 46 "/home/user"], 47 "chronos-access": [], 48 "cras": [], 49 "cros-disks": [], 50 "daemon": [], 51 "debugd": [], 52 "dhcp": ["/encrypted/var/lib/dhcpcd"], 53 "input": [], 54 "ipsec": [], 55 "lp": [], 56 "messagebus": [], 57 "mtp": [], 58 "news": [], 59 "nobody": [], 60 "ntfs-3g": [], 61 "openvpn": [], 62 "portage": ["/encrypted/var/log/emerge.log"], 63 "power": ["/encrypted/var/lib/power_manager", 64 "/encrypted/var/log/power_manager"], 65 "pkcs11": [], 66 "root": None, 67 "sshd": [], 68 "syslog": ["/encrypted/var/log"], 69 "tcpdump": [], 70 "tlsdate": [], 71 "tss": ["/var/lib/tpm"], 72 "uucp": [], 73 "wpa": [], 74 "xorg": ["/encrypted/var/lib/xkb", 75 "/encrypted/var/log/xorg", 76 "/encrypted/var/log/Xorg.0.log"] 77 } 78 79 80 def systemwide_exclusions(self): 81 """Returns a list of paths that are only present on test images 82 and therefore should be excluded from all 'find' commands. 83 """ 84 paths = [] 85 86 # 'preserve/log' is test-only. 87 paths.append("/unencrypted/preserve/log") 88 89 # Cover up Portage noise. 90 paths.append("/encrypted/var/cache/edb") 91 paths.append("/encrypted/var/lib/gentoo") 92 paths.append("/encrypted/var/log/portage") 93 94 # Cover up Autotest noise. 95 paths.append("/dev_image") 96 paths.append("/var_overlay") 97 98 return paths 99 100 101 def generate_prune_arguments(self, prunelist): 102 """Returns a command-line fragment to make 'find' exclude the entries 103 in |prunelist|. 104 105 @param prunelist: list of paths to ignore 106 """ 107 fragment = "-path STATEFUL_ROOT%s -prune -o" 108 fragments = [fragment % path for path in prunelist] 109 return " ".join(fragments) 110 111 112 def generate_find(self, user, prunelist): 113 """ 114 Generates the "find" command that spits out all files in stateful 115 writable by a given user, with the given list of directories removed. 116 117 @param user: report writable paths owned by this user 118 @param prunelist: list of paths to ignore 119 """ 120 if prunelist is None: 121 return "true" # return a no-op shell command, e.g. for root. 122 123 # Exclude world-writeable stuff. 124 # '/var/lib/metrics/uma-events' is world-writeable: crbug.com/198054. 125 prunelist.append("/encrypted/var/lib/metrics/uma-events") 126 # '/run/lock' is world-writeable. 127 prunelist.append("/encrypted/var/lock") 128 # '/var/log/asan' should be world-writeable: crbug.com/453579 129 prunelist.append("/encrypted/var/log/asan") 130 131 # Add system-wide exclusions. 132 prunelist.extend(self.systemwide_exclusions()) 133 134 cmd = "find STATEFUL_ROOT " 135 cmd += self.generate_prune_arguments(prunelist) 136 # Note that we don't "prune" all of /var/tmp's contents, just mask 137 # the dir itself. Any contents are still interesting. 138 cmd += " -path STATEFUL_ROOT/encrypted/var/tmp -o " 139 cmd += " -writable -ls -o -user %s -ls 2>/dev/null" % user 140 return cmd 141 142 143 def expected_owners(self): 144 """Returns the set of file/directory owners expected in stateful.""" 145 # In other words, this is basically the users mentioned in 146 # tests_byuser, except for any expected to actually own zero files. 147 # Currently, there's no exclusions. 148 return set(self._masks_byuser.keys()) 149 150 151 def observed_owners(self): 152 """Returns the set of file/directory owners present in stateful.""" 153 154 cmd = "find STATEFUL_ROOT " 155 cmd += self.generate_prune_arguments(self.systemwide_exclusions()) 156 cmd += " -printf '%u\\n' | sort -u" 157 return set(self.subst_run(cmd).splitlines()) 158 159 160 def owners_lacking_coverage(self): 161 """ 162 Determines the set of owners not covered by any of the 163 per-owner tests implemented in this class. Returns 164 a set of usernames (possibly the empty set). 165 """ 166 return self.observed_owners().difference(self.expected_owners()) 167 168 169 def log_owned_files(self, owner): 170 """ 171 Sends information about all files in the stateful partition 172 owned by a given owner to the standard logging facility. 173 174 @param owner: paths owned by this user will be reported 175 """ 176 cmd = "find STATEFUL_ROOT -user %s -ls" % owner 177 cmd_output = self.subst_run(cmd) 178 logging.error(cmd_output) 179 180 181 def subst_run(self, cmd, stateful_root=_STATEFUL_ROOT): 182 """ 183 Replace "STATEFUL_ROOT" with the actual stateful partition path. 184 185 @param cmd: string containing the command to examine 186 @param stateful_root: path used to replace "STATEFUL_ROOT" 187 """ 188 cmd = cmd.replace("STATEFUL_ROOT", stateful_root) 189 return utils.system_output(cmd, ignore_status=True) 190 191 192 def run_once(self): 193 """ 194 Accounts for the contents of the stateful partition 195 piece-wise, inspecting the level of access which can 196 be obtained by each of the privilege levels (usernames) 197 used in CrOS. 198 199 The test passes if each of the owner-specific sub-tests pass, 200 and if there are no files unaccounted for (i.e., no unexpected 201 file-owners for which we have no tests.) 202 """ 203 testfail = False 204 205 unexpected_owners = self.owners_lacking_coverage() 206 if unexpected_owners: 207 testfail = True 208 for o in unexpected_owners: 209 self.log_owned_files(o) 210 211 # Now run the sub-tests. 212 for user, mask in self._masks_byuser.items(): 213 cmd = self.generate_find(user, mask) 214 215 try: 216 pwd.getpwnam(user) 217 except KeyError, err: 218 logging.warning('Skipping missing user: %s', err) 219 continue 220 221 # The 'EOF' below helps us distinguish 2 types of failures. 222 # We have to use ignore_status=True because many of these 223 # find-based tests will exit(nonzero) to signal that they 224 # encountered inaccessible directories, which we expect by-design. 225 # This creates ambiguity as to whether empty output means 226 # the test ran, and passed, or the su failed. Including an 227 # expected 'EOF' output disambiguates these cases. 228 cmd = "su -s /bin/sh -c '%s;echo EOF' %s" % (cmd, user) 229 cmd_output = self.subst_run(cmd) 230 231 if not cmd_output: 232 # we never got 'EOF', su failed 233 testfail = True 234 logging.error("su failed while attempting to run:") 235 logging.error(cmd) 236 logging.error("[Got %s]", cmd_output) 237 elif not re.search("^\s*EOF\s*$", cmd_output): 238 # we got test failures before 'EOF' 239 testfail = True 240 logging.error("Test for '%s' found unexpected files:\n%s", 241 user, cmd_output) 242 243 if testfail: 244 raise error.TestFail("Unexpected files/perms in stateful") 245