Home | History | Annotate | Download | only in security_StatefulPermissions
      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