| #!/usr/bin/python |
| # |
| # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| __author__ = 'kdlucas@chromium.org (Kelly Lucas)' |
| |
| import logging |
| import os |
| import re |
| import shutil |
| import stat |
| |
| from autotest_lib.client.bin import test |
| from autotest_lib.client.common_lib import error |
| |
| class platform_FilePerms(test.test): |
| """ |
| Test file permissions. |
| """ |
| version = 2 |
| mount_path = '/bin/mount' |
| standard_options = ['nosuid', 'nodev', 'noexec'] |
| standard_rw_options = ['rw'] + standard_options |
| standard_ro_options = ['ro'] + standard_options |
| # When adding an expectation that isn't simply "standard_*_options", |
| # please leave either an explanation for why that mount is special, |
| # or a bug number tracking work to harden that mount point, in a comment. |
| expected_mount_options = { |
| '/dev': { |
| 'type': 'devtmpfs', |
| 'options': ['rw', 'nosuid', 'noexec', 'mode=755']}, |
| '/dev/pstore': { |
| 'type': 'pstore', |
| 'options': standard_rw_options}, |
| '/dev/pts': { # Special case, we want to track gid/mode too. |
| 'type': 'devpts', |
| 'options': ['rw', 'nosuid', 'noexec', 'gid=5', 'mode=620']}, |
| '/dev/shm': {'type': 'tmpfs', 'options': standard_rw_options}, |
| '/home': {'type': 'ext4', 'options': standard_rw_options}, |
| '/home/chronos': {'type': 'ext4', 'options': standard_rw_options}, |
| '/media': {'type': 'tmpfs', 'options': standard_rw_options}, |
| '/mnt/stateful_partition': { |
| 'type': 'ext4', |
| 'options': standard_rw_options}, |
| '/mnt/stateful_partition/encrypted': { |
| 'type': 'ext4', |
| 'options': standard_rw_options}, |
| # Special case - Android container has devices and suid programs. |
| # Note that after the user logs in we remount it as "exec", |
| # therefore we do not enforce 'noexec'. |
| '/opt/google/containers/android/rootfs/root': { |
| 'type': 'squashfs', |
| 'options': ['ro']}, |
| '/opt/google/containers/android/rootfs/root/vendor': { |
| 'type': 'squashfs', |
| 'options': ['ro', 'nosuid', 'nodev']}, |
| '/opt/google/containers/arc-sdcard/mountpoints/container-root': { |
| 'type': 'squashfs', |
| 'options': ['ro', 'noexec']}, |
| '/opt/google/containers/arc-downloads-filesystem/mountpoints/container-root': { |
| 'type': 'squashfs', |
| 'options': ['ro', 'noexec']}, |
| '/opt/google/containers/arc-obb-mounter/mountpoints/container-root': { |
| 'type': 'squashfs', |
| 'options': ['ro', 'noexec']}, |
| '/opt/google/containers/arc-removable-media/mountpoints/container-root': { |
| 'type': 'squashfs', |
| 'options': ['ro', 'noexec']}, |
| '/run/arc/obb': { |
| 'type': 'tmpfs', |
| 'options': standard_ro_options + ['mode=755']}, |
| '/proc': { 'type': 'proc', 'options': standard_rw_options}, |
| '/run': { # Special case, we want to track mode too. |
| 'type': 'tmpfs', |
| 'options': standard_rw_options + ['mode=755']}, |
| # Special case, we want to track group/mode too. |
| # gid 605 == debugfs-access |
| '/run/debugfs_gpu': { |
| 'type': 'debugfs', |
| 'options': standard_rw_options + ['gid=605', 'mode=750']}, |
| '/run/lock': {'type': 'tmpfs', 'options': standard_rw_options}, |
| '/sys': {'type': 'sysfs', 'options': standard_rw_options}, |
| '/sys/fs/cgroup': { |
| 'type': 'tmpfs', |
| 'options': standard_rw_options + ['mode=755']}, |
| '/sys/fs/cgroup/cpu': { |
| 'type': 'cgroup', |
| 'options': standard_rw_options}, |
| '/sys/fs/cgroup/cpuacct': { |
| 'type': 'cgroup', |
| 'options': standard_rw_options}, |
| '/sys/fs/cgroup/cpuset': { |
| 'type': 'cgroup', |
| 'options': standard_rw_options}, |
| '/sys/fs/cgroup/devices': { |
| 'type': 'cgroup', |
| 'options': standard_rw_options}, |
| '/sys/fs/cgroup/freezer': { |
| 'type': 'cgroup', |
| 'options': standard_rw_options}, |
| '/sys/fs/fuse/connections': { |
| 'type': 'fusectl', |
| 'options': standard_rw_options}, |
| '/sys/fs/selinux': { |
| 'type': 'selinuxfs', |
| 'options': ['rw', 'nosuid', 'noexec']}, |
| '/sys/kernel/debug': { |
| 'type': 'debugfs', |
| 'options': standard_rw_options}, |
| '/sys/kernel/debug/tracing': { |
| 'type': 'tracefs', |
| 'options': standard_rw_options}, |
| '/tmp': {'type': 'tmpfs', 'options': standard_rw_options}, |
| '/var': {'type': 'ext4', 'options': standard_rw_options}, |
| '/usr/share/oem': { |
| 'type': 'ext4', |
| 'options': standard_ro_options}, |
| } |
| testmode_modded_fses = set(['/home', '/tmp', '/usr/local']) |
| |
| |
| def checkid(self, fs, userid): |
| """ |
| Check that the uid and gid for |fs| match |userid|. |
| |
| @param fs: string, directory or file path. |
| @param userid: userid to check for. |
| Returns: |
| int, the number errors (non-matches) detected. |
| """ |
| errors = 0 |
| |
| if not os.access(fs, os.F_OK): |
| # The path does not exist, so exit early. |
| return errors |
| |
| uid = os.stat(fs)[stat.ST_UID] |
| gid = os.stat(fs)[stat.ST_GID] |
| |
| if userid != uid: |
| logging.error('Wrong uid in filesystem "%s"', fs) |
| errors += 1 |
| if userid != gid: |
| logging.error('Wrong gid in filesystem "%s"', fs) |
| errors += 1 |
| |
| return errors |
| |
| |
| def get_perm(self, fs): |
| """ |
| Check the file permissions of a filesystem. |
| |
| @param fs: string, mount point for filesystem to check. |
| Returns: |
| int, equivalent to unix permissions. |
| """ |
| MASK = 0777 |
| |
| if not os.access(fs, os.F_OK): |
| # The path does not exist, so exit early. |
| return None |
| |
| fstat = os.stat(fs) |
| mode = fstat[stat.ST_MODE] |
| |
| fperm = oct(mode & MASK) |
| return fperm |
| |
| |
| def read_mtab(self, mtab_path='/etc/mtab'): |
| """ |
| Helper function to read the mtab file into a dict |
| |
| @param mtab_path: path to '/etc/mtab' |
| Returns: |
| dict, mount points as keys, and another dict with |
| options list, device and type as values. |
| """ |
| file_handle = open(mtab_path, 'r') |
| lines = file_handle.readlines() |
| file_handle.close() |
| |
| # Save mtab to the results dir to diagnose failures. |
| shutil.copyfile(mtab_path, |
| os.path.join(self.resultsdir, |
| os.path.basename(mtab_path))) |
| |
| comment_re = re.compile("#.*$") |
| mounts = {} |
| for line in lines: |
| # remove any comments first |
| line = comment_re.sub("", line) |
| fields = line.split() |
| # ignore malformed lines |
| if len(fields) < 4: |
| continue |
| # Don't include rootfs in the list, because it maps to the |
| # same location as /dev/root: '/' (and we don't care about |
| # its options at the moment). |
| if fields[0] == 'rootfs': |
| continue |
| mounts[fields[1]] = {'device': fields[0], |
| 'type': fields[2], |
| 'options': fields[3].split(',')} |
| return mounts |
| |
| |
| def try_write(self, fs): |
| """ |
| Try to write a file in the given filesystem. |
| |
| @param fs: string, file system to use. |
| Returns: |
| int, number of errors encountered: |
| 0 = write successful, |
| >0 = write not successful. |
| """ |
| |
| TEXT = 'This is filler text for a test file.\n' |
| |
| tempfile = os.path.join(fs, 'test') |
| try: |
| fh = open(tempfile, 'w') |
| fh.write(TEXT) |
| fh.close() |
| except OSError: # This error will occur with read only filesystem. |
| return 1 |
| except IOError, e: |
| return 1 |
| |
| if os.path.exists(tempfile): |
| os.remove(tempfile) |
| |
| return 0 |
| |
| |
| def check_mounted_read_only(self, filesystem): |
| """ |
| Check the permissions of a filesystem according to /etc/mtab. |
| |
| @param filesystem: string, file system device to check. |
| Returns: |
| 1 if rw, 0 if ro |
| """ |
| |
| errors = 0 |
| mtab = self.read_mtab() |
| if not (filesystem in mtab.keys()): |
| logging.error('Could not find filesystem "%s" in mtab', filesystem) |
| errors += 1 |
| return errors # no point in continuing this test. |
| if not ('ro' in mtab[filesystem]['options']): |
| logging.error('Filesystem "%s" is not mounted read-only', |
| filesystem) |
| errors += 1 |
| return errors |
| |
| |
| def check_mount_options(self): |
| """ |
| Check the permissions of all non-rootfs filesystems to make |
| sure they have the right mount options. In order to do this, |
| both the live system state, and a log-snapshot of what the system |
| looked like prior to dev-mode/test-mode modifications were applied, |
| are validated. |
| |
| Note that since this test is not a UITest, and takes place |
| while the system waits at a login screen, mount options are |
| not checked for a mounted cryptohome or guestfs. Consult the |
| security_ProfilePermissions test for those checks. |
| |
| Args: |
| (none) |
| Returns: |
| int, the number of errors identified in mount options. |
| """ |
| errors = 0 |
| # Perform mount-option checks of both mount options as |
| # captured during boot, and, the live system state. After the |
| # first pass (where we process mount_options.log), grow the |
| # list of ignored filesystems to include all those we know are |
| # tweaked by devmode/mod-for-test mode. This properly sets |
| # expectations for the second pass. |
| mtabs = ['/var/log/mount_options.log', '/etc/mtab'] |
| ignored_fses = set(['/']) |
| ignored_types = set(['ecryptfs']) |
| for mtab_path in mtabs: |
| mtab = self.read_mtab(mtab_path=mtab_path) |
| for fs in mtab.keys(): |
| if fs in ignored_fses: |
| continue |
| |
| fs_type = mtab[fs]['type'] |
| if fs_type in ignored_types: |
| logging.warning('Ignoring filesystem "%s" with type "%s"', |
| fs, fs_type) |
| continue |
| if not fs in self.expected_mount_options: |
| logging.error('No expectations entry for "%s"', fs) |
| errors += 1 |
| continue |
| |
| if fs_type != self.expected_mount_options[fs]['type']: |
| logging.error( |
| '[%s] "%s" has type "%s", expected type "%s"', |
| mtab_path, fs, fs_type, |
| self.expected_mount_options[fs]['type']) |
| errors += 1 |
| |
| # For options, require the specified options to be present. |
| # Do not consider it an error if extra options are present. |
| # (This makes it easy to deal with options we don't wish |
| # to track closely, like devtmpfs's nr_inodes= for example.) |
| seen = set(mtab[fs]['options']) |
| expected = set(self.expected_mount_options[fs]['options']) |
| missing = expected - seen |
| if (missing): |
| logging.error('[%s] "%s" is missing options "%s"', |
| mtab_path, fs, missing) |
| errors += 1 |
| |
| ignored_fses.update(self.testmode_modded_fses) |
| |
| return errors |
| |
| |
| def run_once(self): |
| """ |
| Main testing routine for platform_FilePerms. |
| """ |
| errors = 0 |
| |
| # Root owned directories with expected permissions. |
| root_dirs = {'/': ['0755'], |
| '/bin': ['0755'], |
| '/boot': ['0755'], |
| '/dev': ['0755'], |
| '/etc': ['0755'], |
| '/home': ['0755'], |
| '/lib': ['0755'], |
| '/media': ['0777'], |
| '/mnt': ['0755'], |
| '/mnt/stateful_partition': ['0755'], |
| '/opt': ['0755'], |
| '/proc': ['0555'], |
| '/run': ['0755'], |
| '/sbin': ['0755'], |
| '/sys': ['0555', '0755'], |
| '/tmp': ['0777'], |
| '/usr': ['0755'], |
| '/usr/bin': ['0755'], |
| '/usr/lib': ['0755'], |
| '/usr/local': ['0755'], |
| '/usr/sbin': ['0755'], |
| '/usr/share': ['0755'], |
| '/var': ['0755'], |
| '/var/cache': ['0755']} |
| |
| # Read-only directories |
| ro_dirs = ['/', '/bin', '/boot', '/etc', '/lib', '/mnt', |
| '/opt', '/sbin', '/usr', '/usr/bin', '/usr/lib', |
| '/usr/sbin', '/usr/share'] |
| |
| # Root directories writable by root |
| root_rw_dirs = ['/run', '/var', '/var/lib', '/var/cache', '/var/log', |
| '/usr/local'] |
| |
| # Ensure you cannot write files in read only directories. |
| for dir in ro_dirs: |
| if self.try_write(dir) == 0: |
| logging.error('Root can write to read-only dir "%s"', dir) |
| errors += 1 |
| |
| # Ensure the uid and gid are correct for root owned directories. |
| for dir in root_dirs: |
| if self.checkid(dir, 0) > 0: |
| errors += 1 |
| |
| # Ensure root can write into root dirs with rw access. |
| for dir in root_rw_dirs: |
| if self.try_write(dir) > 0: |
| errors += 1 |
| |
| # Check permissions on root owned directories. |
| for dir in root_dirs: |
| fperms = self.get_perm(dir) |
| if fperms is not None and fperms not in root_dirs[dir]: |
| logging.error('"%s" has "%s" permissions', dir, fperms) |
| errors += 1 |
| |
| errors += self.check_mounted_read_only('/') |
| |
| # Check mount options on mount points. |
| errors += self.check_mount_options() |
| |
| # If errors is not zero, there were errors. |
| if errors > 0: |
| raise error.TestFail('Found %d permission errors' % errors) |