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