# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import pwd import re from autotest_lib.client.bin import test, utils from autotest_lib.client.common_lib import error class security_StatefulPermissions(test.test): """ Report all unexpected writable paths in the /mnt/stateful_partition tree. """ version = 1 _STATEFUL_ROOT = "/mnt/stateful_partition" _AID_SYSTEM = 1000 _AID_CACHE = 2001 # Note that chronos permissions in /home are covered in greater detail # by 'security_ProfilePermissions'. _masks_byuser = {"adm": [], "android-root": ["/encrypted/var/log/android.kmsg"], "attestation": ["/unencrypted/preserve", "/unencrypted/preserve/attestation.epb"], "avfs": [], "bin": [], "bluetooth": ["/encrypted/var/lib/bluetooth"], "chaps": ["/encrypted/var/lib/chaps"], "chronos": ["/encrypted/chronos", "/encrypted/var/cache/app_pack", "/encrypted/var/cache/device_local_account_component_policy", "/encrypted/var/cache/device_local_account_extensions", "/encrypted/var/cache/device_local_account_external_policy_data", "/encrypted/var/cache/display_profiles", "/encrypted/var/cache/echo", "/encrypted/var/cache/external_cache", "/encrypted/var/cache/shared_extensions", "/encrypted/var/cache/signin_profile_component_policy", "/encrypted/var/cache/touch_trial/selection", "/encrypted/var/lib/cromo", "/encrypted/var/lib/opencryptoki", "/encrypted/var/lib/timezone", "/encrypted/var/lib/Synaptics/chronos.1000", "/encrypted/var/log/chrome", "/encrypted/var/log/connectivity.bak", "/encrypted/var/log/connectivity.log", "/encrypted/var/log/metrics", "/encrypted/var/minidumps", "/home/user"], "chronos-access": [], "cras": [], "cros-disks": [], "cups": ["/encrypted/var/cache/cups", "/encrypted/var/spool/cups"], "daemon": [], "debugd": [], "dhcp": ["/encrypted/var/lib/dhcpcd"], "dlm": ["/encrypted/var/log/displaylink"], "imageloaderd": ["/encrypted/var/lib/imageloader"], "input": [], "ipsec": [], "lp": [], "messagebus": [], "mtp": [], "news": [], "nobody": [], "ntfs-3g": [], "openvpn": [], "portage": ["/encrypted/var/log/emerge.log"], "power": ["/encrypted/var/lib/power_manager", "/encrypted/var/log/power_manager"], "pkcs11": [], "root": None, "sshd": [], "syslog": ["/encrypted/var/log"], "tcpdump": [], "tlsdate": [], "tpm_manager": ["/encrypted/var/lib/tpm_manager", "/unencrypted/preserve"], "tss": ["/var/lib/tpm"], "uucp": [], "wpa": [], "xorg": ["/encrypted/var/lib/xkb", "/encrypted/var/log/xorg", "/encrypted/var/log/Xorg.0.log"] } def systemwide_exclusions(self): """Returns a list of paths that are only present on test images and therefore should be excluded from all 'find' commands. """ paths = [] # 'preserve/log' is test-only. paths.append("/unencrypted/preserve/log") # Cover up Portage noise. paths.append("/encrypted/var/cache/edb") paths.append("/encrypted/var/lib/gentoo") paths.append("/encrypted/var/log/portage") # Cover up Autotest noise. paths.append("/dev_image") paths.append("/var_overlay") return paths def generate_prune_arguments_android(self): """Returns a command-line fragment to make 'find' exclude android-data/cache: uid=AID_SYSTEM, gid=AID_CACHE android-data/data: uid=AID_SYSTEM, gid=AID_SYSTEM Files under these paths are created by various android users. """ try: aroot_uid = pwd.getpwnam('android-root').pw_uid except KeyError: # android-root not found, so don't prune anything return "" # On ecryptfs backend, the Android data path is # /home/.shadow/hash/vault/root/ENCRYPTED/... # while on ext4crypto backend, it is: # /home/.shadow/hash/mount/ENCRYPTED/ENCRYPTED/... cmd = "-regextype posix-extended -regex STATEFUL_ROOT/home/.shadow/" cmd += "[[:alnum:]]{40}/(vault/root|mount/[^/]*)/[^/]*/[^/]* " cmd += "-uid {0} \\( -gid {1} -o -gid {2} \\) -prune -o ".format( aroot_uid + self._AID_SYSTEM, aroot_uid + self._AID_SYSTEM, aroot_uid + self._AID_CACHE) return cmd def generate_prune_arguments(self, prunelist): """Returns a command-line fragment to make 'find' exclude the entries in |prunelist|. @param prunelist: list of paths to ignore """ fragment = "-path STATEFUL_ROOT%s -prune -o" fragments = [fragment % path for path in prunelist] return " ".join(fragments) def generate_find(self, user, prunelist): """ Generates the "find" command that spits out all files in stateful writable by a given user, with the given list of directories removed. @param user: report writable paths owned by this user @param prunelist: list of paths to ignore """ if prunelist is None: return "true" # return a no-op shell command, e.g. for root. # Exclude world-writeable stuff. # '/var/lib/metrics/uma-events' is world-writeable: crbug.com/198054. prunelist.append("/encrypted/var/lib/metrics/uma-events") # '/run/lock' is world-writeable. prunelist.append("/encrypted/var/lock") # '/var/log/asan' should be world-writeable: crbug.com/453579 prunelist.append("/encrypted/var/log/asan") # Add system-wide exclusions. prunelist.extend(self.systemwide_exclusions()) cmd = "find STATEFUL_ROOT " cmd += self.generate_prune_arguments(prunelist) # Note that we don't "prune" all of /var/tmp's contents, just mask # the dir itself. Any contents are still interesting. cmd += " -path STATEFUL_ROOT/encrypted/var/tmp -o " cmd += " -writable -ls -o -user %s -ls 2>/dev/null" % user return cmd def expected_owners(self): """Returns the set of file/directory owners expected in stateful.""" # In other words, this is basically the users mentioned in # tests_byuser, except for any expected to actually own zero files. # Currently, there's no exclusions. return set(self._masks_byuser.keys()) def observed_owners(self): """Returns the set of file/directory owners present in stateful.""" cmd = "find STATEFUL_ROOT " cmd += self.generate_prune_arguments_android() cmd += self.generate_prune_arguments(self.systemwide_exclusions()) cmd += " -printf '%u\\n' | sort -u" return set(self.subst_run(cmd).splitlines()) def owners_lacking_coverage(self): """ Determines the set of owners not covered by any of the per-owner tests implemented in this class. Returns a set of usernames (possibly the empty set). """ return self.observed_owners().difference(self.expected_owners()) def log_owned_files(self, owner): """ Sends information about all files in the stateful partition owned by a given owner to the standard logging facility. @param owner: paths owned by this user will be reported """ cmd = "find STATEFUL_ROOT -user %s -ls" % owner cmd_output = self.subst_run(cmd) logging.error(cmd_output) def subst_run(self, cmd, stateful_root=_STATEFUL_ROOT): """ Replace "STATEFUL_ROOT" with the actual stateful partition path. @param cmd: string containing the command to examine @param stateful_root: path used to replace "STATEFUL_ROOT" """ cmd = cmd.replace("STATEFUL_ROOT", stateful_root) return utils.system_output(cmd, ignore_status=True) def run_once(self): """ Accounts for the contents of the stateful partition piece-wise, inspecting the level of access which can be obtained by each of the privilege levels (usernames) used in CrOS. The test passes if each of the owner-specific sub-tests pass, and if there are no files unaccounted for (i.e., no unexpected file-owners for which we have no tests.) """ testfail = False unexpected_owners = self.owners_lacking_coverage() if unexpected_owners: testfail = True for o in unexpected_owners: self.log_owned_files(o) # Now run the sub-tests. for user, mask in self._masks_byuser.items(): cmd = self.generate_find(user, mask) try: pwd.getpwnam(user) except KeyError, err: logging.warning('Skipping missing user: %s', err) continue # The 'EOF' below helps us distinguish 2 types of failures. # We have to use ignore_status=True because many of these # find-based tests will exit(nonzero) to signal that they # encountered inaccessible directories, which we expect by-design. # This creates ambiguity as to whether empty output means # the test ran, and passed, or the su failed. Including an # expected 'EOF' output disambiguates these cases. cmd = "su -s /bin/sh -c '%s;echo EOF' %s" % (cmd, user) cmd_output = self.subst_run(cmd) if not cmd_output: # we never got 'EOF', su failed testfail = True logging.error("su failed while attempting to run:") logging.error(cmd) logging.error("[Got %s]", cmd_output) elif not re.search("^\s*EOF\s*$", cmd_output): # we got test failures before 'EOF' testfail = True logging.error("Test for '%s' found unexpected files:\n%s", user, cmd_output) if testfail: raise error.TestFail("Unexpected files/perms in stateful")