blob: 70b9d27f2ec151e69ec43dc42e04c2415db8b304 [file] [log] [blame]
#!/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 stat
import subprocess
from autotest_lib.client.bin import utils, test
from autotest_lib.client.common_lib import error
class platform_FilePerms(test.test):
"""
Test file permissions.
"""
version = 2
mount_path = '/bin/mount'
standard_rw_options = ['rw', 'nosuid', 'nodev', 'noexec', 'relatime']
# When adding an expectation that isn't simply "standard_rw_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': { # crosbug.com/32629
'type': 'devtmpfs',
'options': ['rw', 'relatime', 'mode=755']},
'/dev/pstore': { # crosbug.com/32630
'type': 'pstore',
'options': ['rw', 'relatime']},
'/dev/pts': { # Special case, we want to track gid/mode too.
'type': 'devpts',
'options': ['rw', 'nosuid', 'noexec', 'relatime', '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},
'/proc': {'type': 'proc', 'options': standard_rw_options},
'/sys': {'type': 'sysfs', 'options': standard_rw_options},
'/sys/fs/fuse/connections': { # crosbug.com/32631
'type': 'fusectl',
'options': ['rw', 'relatime']},
'/sys/kernel/debug': { # crosbug.com/32632
'type': 'debugfs',
'options': ['rw', 'relatime']},
'/tmp': {'type': 'tmpfs', 'options': standard_rw_options},
'/tmp/cgroup/cpu': { # crosbug.com/32633
'type': 'cgroup',
'options': ['rw', 'relatime', 'cpu']},
'/var': {'type': 'ext4', 'options': standard_rw_options},
'/var/lock': {'type': 'tmpfs', 'options': standard_rw_options},
'/var/run': { # Special case, we want to track mode too.
'type': 'tmpfs',
'options': ['rw', 'nosuid', 'nodev', 'noexec', 'relatime',
'mode=755']},
}
testmode_modded_fses = set(['/home', '/tmp', '/usr/local'])
def checkid(self, fs, userid):
"""
Check that the uid and gid for fs match userid.
Args:
fs: string, directory or file path.
userid: userid to check for.
Returns:
int, the number errors (non-matches) detected.
"""
errors = 0
uid = os.stat(fs)[stat.ST_UID]
gid = os.stat(fs)[stat.ST_GID]
if userid != uid:
logging.warn('fs %s uid wrong' % fs)
errors += 1
if userid != gid:
logging.warn('fs %s gid wrong' % fs)
errors += 1
return errors
def get_perm(self, fs):
"""
Check the file permissions of filesystem.
Args:
fs: string, mount point for filesystem to check.
Returns:
int, equivalent to unix permissions.
"""
MASK = 0777
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
Args:
(none)
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()
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.
Args:
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.
Args:
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.warn('Did 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.warn('Filesystem "%s" is not mounted read only!' %
filesystem)
errors += 1
return errors
def check_mount_setup(self):
"""
Check to make sure that mount shows the same mount points and
options as /etc/mtab, and that /etc/mtab is a symlink to
/proc/mount.
Args:
(none)
Returns:
int, Number of errors encountered.
"""
# Call mount, and compare output to /etc/mtab entries.
mount_call = subprocess.Popen([self.mount_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = mount_call.communicate();
mount_lines = out.split("\n");
mounts = {}
for line in mount_lines:
fields = line.split()
# Skip empty/non-conforming lines.
if (len(fields) >= 6):
mounts[fields[2]] = {
'device': fields[0],
'type': fields[4],
'options': fields[5].strip("()").split(','),
}
# Compare the filesystems to make sure that what mount
# reports, and what's in the config file are identical in both
# the mounts that exist, and the options that are set. Note
# that this test allows there to be listings in mtab that are
# not currently mounted.
errors = 0
mtab = self.read_mtab()
for filesystem in mounts.keys():
if not (filesystem in mtab.keys()):
logging.warn('Mounted filesystem %s not found in mtab.' %
filesystem)
errors += 1
for option in mounts[filesystem]['options']:
if not (option in mtab[filesystem]['options']):
logging.warn('Mounted filesystem %s has option %s '
'that is not listed in mtab.' %
(filesystem, option))
errors +=1
for option in mtab[filesystem]['options']:
if not (option in mounts[filesystem]['options']):
logging.warn('Mounted filesystem %s does not have '
'option %s that is listed in mtab.' %
(filesystem, option))
errors +=1
# Now we just make sure that /etc/mtab is a symlink to /proc/mounts
mtab_link = os.readlink("/etc/mtab")
if mtab_link != "/proc/mounts":
logging.warn('Symbolic link /etc/mtab points to "%s" instead of '
'/proc/mounts.' % mtab_link)
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(['/'])
for mtab_path in mtabs:
mtab = self.read_mtab(mtab_path=mtab_path)
for fs in mtab.keys():
if fs in ignored_fses:
continue
if not fs in self.expected_mount_options:
logging.warn('No expectations entry for %s' % fs)
errors += 1
continue
if mtab[fs]['type'] != self.expected_mount_options[fs]['type']:
logging.warn('[%s] Wrong fs type: %s is %s, expected %s' %
(mtab_path, fs, mtab[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.warn('[%s] Missing options: %s is missing %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'],
'/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 = ['/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.warn('Root can write to RO 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:
logging.warn('Root cannot write RW dir %s' % dir)
errors += 1
# Check permissions on root owned directories.
for dir in root_dirs:
fperms = self.get_perm(dir)
if fperms not in root_dirs[dir]:
logging.warn('%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()
# Check that /bin/mount output and mtab jive,
# and that mtab is a symbolic link to /proc/mounts.
errors += self.check_mount_setup()
# If errors is not zero, there were errors.
if errors > 0:
raise error.TestFail('Found %d permission errors' % errors)