Home | History | Annotate | Download | only in platform_FilePerms
      1 #!/usr/bin/python
      2 #
      3 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 __author__ = 'kdlucas (at] chromium.org (Kelly Lucas)'
      8 
      9 import logging
     10 import os
     11 import re
     12 import shutil
     13 import stat
     14 
     15 from autotest_lib.client.bin import test
     16 from autotest_lib.client.common_lib import error
     17 
     18 class platform_FilePerms(test.test):
     19     """
     20     Test file permissions.
     21     """
     22     version = 2
     23     mount_path = '/bin/mount'
     24     standard_rw_options = ['rw', 'nosuid', 'nodev', 'noexec', 'relatime']
     25     # When adding an expectation that isn't simply "standard_rw_options,"
     26     # please leave either an explanation for why that mount is special,
     27     # or a bug number tracking work to harden that mount point, in a comment.
     28     expected_mount_options = {
     29         '/dev': {
     30             'type': 'devtmpfs',
     31             'options': ['rw', 'nosuid', 'noexec', 'relatime', 'mode=755']},
     32         '/dev/pstore': {
     33             'type': 'pstore',
     34             'options': standard_rw_options},
     35         '/dev/pts': { # Special case, we want to track gid/mode too.
     36             'type': 'devpts',
     37             'options': ['rw', 'nosuid', 'noexec', 'relatime', 'gid=5',
     38                         'mode=620']},
     39         '/dev/shm': {'type': 'tmpfs', 'options': standard_rw_options},
     40         '/home': {'type': 'ext4', 'options': standard_rw_options},
     41         '/home/chronos': {'type': 'ext4', 'options': standard_rw_options},
     42         '/media': {'type': 'tmpfs', 'options': standard_rw_options},
     43         '/mnt/stateful_partition': {
     44             'type': 'ext4',
     45             'options': standard_rw_options},
     46         '/mnt/stateful_partition/encrypted': {
     47             'type': 'ext4',
     48             'options': standard_rw_options},
     49         '/proc': {'type': 'proc', 'options': standard_rw_options},
     50         '/run': { # Special case, we want to track mode too.
     51             'type': 'tmpfs',
     52             'options': ['rw', 'nosuid', 'nodev', 'noexec', 'relatime',
     53                         'mode=755']},
     54         # Special case, we want to track group/mode too.
     55         # gid 236 == debugfs-access
     56         '/run/debugfs_gpu': {
     57             'type': 'debugfs',
     58             'options': ['rw', 'nosuid', 'nodev', 'noexec', 'relatime',
     59                         'gid=236', 'mode=750']},
     60         '/run/lock': {'type': 'tmpfs', 'options': standard_rw_options},
     61         '/sys': {'type': 'sysfs', 'options': standard_rw_options},
     62         '/sys/fs/cgroup': {
     63             'type': 'tmpfs',
     64             'options': standard_rw_options + ['mode=755']},
     65         '/sys/fs/cgroup/cpu': {
     66             'type': 'cgroup',
     67             'options': standard_rw_options},
     68         '/sys/fs/cgroup/cpuacct': {
     69             'type': 'cgroup',
     70             'options': standard_rw_options},
     71         '/sys/fs/cgroup/devices': {
     72             'type': 'cgroup',
     73             'options': standard_rw_options},
     74         '/sys/fs/cgroup/freezer': {
     75             'type': 'cgroup',
     76             'options': standard_rw_options},
     77         '/sys/fs/fuse/connections': {
     78             'type': 'fusectl',
     79             'options': standard_rw_options},
     80         '/sys/kernel/debug': {
     81             'type': 'debugfs',
     82             'options': standard_rw_options},
     83         '/tmp': {'type': 'tmpfs', 'options': standard_rw_options},
     84         '/var': {'type': 'ext4', 'options': standard_rw_options},
     85         '/usr/share/oem': {
     86             'type': 'ext4',
     87             'options': ['ro', 'nosuid', 'nodev', 'noexec', 'relatime']},
     88     }
     89     testmode_modded_fses = set(['/home', '/tmp', '/usr/local'])
     90 
     91 
     92     def checkid(self, fs, userid):
     93         """
     94         Check that the uid and gid for |fs| match |userid|.
     95 
     96         @param fs: string, directory or file path.
     97         @param userid: userid to check for.
     98         Returns:
     99             int, the number errors (non-matches) detected.
    100         """
    101         errors = 0
    102 
    103         if not os.access(fs, os.F_OK):
    104             # The path does not exist, so exit early.
    105             return errors
    106 
    107         uid = os.stat(fs)[stat.ST_UID]
    108         gid = os.stat(fs)[stat.ST_GID]
    109 
    110         if userid != uid:
    111             logging.error('Wrong uid in filesystem "%s"', fs)
    112             errors += 1
    113         if userid != gid:
    114             logging.error('Wrong gid in filesystem "%s"', fs)
    115             errors += 1
    116 
    117         return errors
    118 
    119 
    120     def get_perm(self, fs):
    121         """
    122         Check the file permissions of a filesystem.
    123 
    124         @param fs: string, mount point for filesystem to check.
    125         Returns:
    126             int, equivalent to unix permissions.
    127         """
    128         MASK = 0777
    129 
    130         if not os.access(fs, os.F_OK):
    131             # The path does not exist, so exit early.
    132             return None
    133 
    134         fstat = os.stat(fs)
    135         mode = fstat[stat.ST_MODE]
    136 
    137         fperm = oct(mode & MASK)
    138         return fperm
    139 
    140 
    141     def read_mtab(self, mtab_path='/etc/mtab'):
    142         """
    143         Helper function to read the mtab file into a dict
    144 
    145         @param mtab_path: path to '/etc/mtab'
    146         Returns:
    147           dict, mount points as keys, and another dict with
    148           options list, device and type as values.
    149         """
    150         file_handle = open(mtab_path, 'r')
    151         lines = file_handle.readlines()
    152         file_handle.close()
    153 
    154         # Save mtab to the results dir to diagnose failures.
    155         shutil.copyfile(mtab_path,
    156                         os.path.join(self.resultsdir,
    157                                      os.path.basename(mtab_path)))
    158 
    159         comment_re = re.compile("#.*$")
    160         mounts = {}
    161         for line in lines:
    162             # remove any comments first
    163             line = comment_re.sub("", line)
    164             fields = line.split()
    165             # ignore malformed lines
    166             if len(fields) < 4:
    167                 continue
    168             # Don't include rootfs in the list, because it maps to the
    169             # same location as /dev/root: '/' (and we don't care about
    170             # its options at the moment).
    171             if fields[0] == 'rootfs':
    172                 continue
    173             mounts[fields[1]] = {'device': fields[0],
    174                                  'type': fields[2],
    175                                  'options': fields[3].split(',')}
    176         return mounts
    177 
    178 
    179     def try_write(self, fs):
    180         """
    181         Try to write a file in the given filesystem.
    182 
    183         @param fs: string, file system to use.
    184         Returns:
    185             int, number of errors encountered:
    186             0 = write successful,
    187             >0 = write not successful.
    188         """
    189 
    190         TEXT = 'This is filler text for a test file.\n'
    191 
    192         tempfile = os.path.join(fs, 'test')
    193         try:
    194             fh = open(tempfile, 'w')
    195             fh.write(TEXT)
    196             fh.close()
    197         except OSError: # This error will occur with read only filesystem.
    198             return 1
    199         except IOError, e:
    200             return 1
    201 
    202         if os.path.exists(tempfile):
    203             os.remove(tempfile)
    204 
    205         return 0
    206 
    207 
    208     def check_mounted_read_only(self, filesystem):
    209         """
    210         Check the permissions of a filesystem according to /etc/mtab.
    211 
    212         @param filesystem: string, file system device to check.
    213         Returns:
    214             1 if rw, 0 if ro
    215         """
    216 
    217         errors = 0
    218         mtab = self.read_mtab()
    219         if not (filesystem in mtab.keys()):
    220             logging.error('Could not find filesystem "%s" in mtab', filesystem)
    221             errors += 1
    222             return errors # no point in continuing this test.
    223         if not ('ro' in mtab[filesystem]['options']):
    224             logging.error('Filesystem "%s" is not mounted read-only',
    225                           filesystem)
    226             errors += 1
    227         return errors
    228 
    229 
    230     def check_mount_options(self):
    231         """
    232         Check the permissions of all non-rootfs filesystems to make
    233         sure they have the right mount options. In order to do this,
    234         both the live system state, and a log-snapshot of what the system
    235         looked like prior to dev-mode/test-mode modifications were applied,
    236         are validated.
    237 
    238         Note that since this test is not a UITest, and takes place
    239         while the system waits at a login screen, mount options are
    240         not checked for a mounted cryptohome or guestfs. Consult the
    241         security_ProfilePermissions test for those checks.
    242 
    243         Args:
    244             (none)
    245         Returns:
    246             int, the number of errors identified in mount options.
    247         """
    248         errors = 0
    249         # Perform mount-option checks of both mount options as
    250         # captured during boot, and, the live system state.  After the
    251         # first pass (where we process mount_options.log), grow the
    252         # list of ignored filesystems to include all those we know are
    253         # tweaked by devmode/mod-for-test mode. This properly sets
    254         # expectations for the second pass.
    255         mtabs = ['/var/log/mount_options.log', '/etc/mtab']
    256         ignored_fses = set(['/'])
    257         ignored_types = set(['ecryptfs'])
    258         for mtab_path in mtabs:
    259             mtab = self.read_mtab(mtab_path=mtab_path)
    260             for fs in mtab.keys():
    261                 if fs in ignored_fses:
    262                     continue
    263 
    264                 fs_type = mtab[fs]['type']
    265                 if fs_type in ignored_types:
    266                     logging.warning('Ignoring filesystem "%s" with type "%s"',
    267                                  fs, fs_type)
    268                     continue
    269                 if not fs in self.expected_mount_options:
    270                     logging.error('No expectations entry for "%s"', fs)
    271                     errors += 1
    272                     continue
    273 
    274                 if fs_type != self.expected_mount_options[fs]['type']:
    275                     logging.error(
    276                             '[%s] "%s" has type "%s", expected type "%s"',
    277                             mtab_path, fs, fs_type,
    278                             self.expected_mount_options[fs]['type'])
    279                     errors += 1
    280 
    281                 # For options, require the specified options to be present.
    282                 # Do not consider it an error if extra options are present.
    283                 # (This makes it easy to deal with options we don't wish
    284                 # to track closely, like devtmpfs's nr_inodes= for example.)
    285                 seen = set(mtab[fs]['options'])
    286                 expected = set(self.expected_mount_options[fs]['options'])
    287                 missing = expected - seen
    288                 if (missing):
    289                     logging.error('[%s] "%s" is missing options "%s"',
    290                                   mtab_path, fs, missing)
    291                     errors += 1
    292 
    293             ignored_fses.update(self.testmode_modded_fses)
    294 
    295         return errors
    296 
    297 
    298     def run_once(self):
    299         """
    300         Main testing routine for platform_FilePerms.
    301         """
    302         errors = 0
    303 
    304         # Root owned directories with expected permissions.
    305         root_dirs = {'/': ['0755'],
    306                      '/bin': ['0755'],
    307                      '/boot': ['0755'],
    308                      '/dev': ['0755'],
    309                      '/etc': ['0755'],
    310                      '/home': ['0755'],
    311                      '/lib': ['0755'],
    312                      '/media': ['0777'],
    313                      '/mnt': ['0755'],
    314                      '/mnt/stateful_partition': ['0755'],
    315                      '/opt': ['0755'],
    316                      '/proc': ['0555'],
    317                      '/run': ['0755'],
    318                      '/sbin': ['0755'],
    319                      '/sys': ['0555', '0755'],
    320                      '/tmp': ['0777'],
    321                      '/usr': ['0755'],
    322                      '/usr/bin': ['0755'],
    323                      '/usr/lib': ['0755'],
    324                      '/usr/local': ['0755'],
    325                      '/usr/sbin': ['0755'],
    326                      '/usr/share': ['0755'],
    327                      '/var': ['0755'],
    328                      '/var/cache': ['0755']}
    329 
    330         # Read-only directories
    331         ro_dirs = ['/', '/bin', '/boot', '/etc', '/lib', '/mnt',
    332                    '/opt', '/sbin', '/usr', '/usr/bin', '/usr/lib',
    333                    '/usr/sbin', '/usr/share']
    334 
    335         # Root directories writable by root
    336         root_rw_dirs = ['/run', '/var', '/var/lib', '/var/cache', '/var/log',
    337                         '/usr/local']
    338 
    339         # Ensure you cannot write files in read only directories.
    340         for dir in ro_dirs:
    341             if self.try_write(dir) == 0:
    342                 logging.error('Root can write to read-only dir "%s"', dir)
    343                 errors += 1
    344 
    345         # Ensure the uid and gid are correct for root owned directories.
    346         for dir in root_dirs:
    347             if self.checkid(dir, 0) > 0:
    348                 errors += 1
    349 
    350         # Ensure root can write into root dirs with rw access.
    351         for dir in root_rw_dirs:
    352             if self.try_write(dir) > 0:
    353                 errors += 1
    354 
    355         # Check permissions on root owned directories.
    356         for dir in root_dirs:
    357             fperms = self.get_perm(dir)
    358             if fperms is not None and fperms not in root_dirs[dir]:
    359                 logging.error('"%s" has "%s" permissions', dir, fperms)
    360                 errors += 1
    361 
    362         errors += self.check_mounted_read_only('/')
    363 
    364         # Check mount options on mount points.
    365         errors += self.check_mount_options()
    366 
    367         # If errors is not zero, there were errors.
    368         if errors > 0:
    369             raise error.TestFail('Found %d permission errors' % errors)
    370