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