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/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