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