blob: 49523ee4ca85920829f51004ca502078895c6140 [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 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
loop_device = r'/dev/loop[0-9]+$'
root_device = r'/dev/root$'
# 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']},
'/sys/fs/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},
'/opt/google/containers/android/rootfs/android-data/data/dalvik-cache/'
'arm': {
'type': ['ext4'],
'options': standard_rw_options},
'/opt/google/containers/android/rootfs/android-data/data/dalvik-cache/'
'x86': {
'type': ['ext4'],
'options': standard_rw_options},
'/opt/google/containers/android/rootfs/android-data/data/dalvik-cache/'
'x86_64': {
'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': {
'device': loop_device,
'type': ['squashfs'],
'options': ['ro']},
'/opt/google/containers/android/rootfs/root/system/lib/arm': {
'device': loop_device,
'type': ['squashfs'],
'options': ['ro', 'nosuid', 'nodev']},
'/opt/google/containers/arc-sdcard/mountpoints/container-root': {
'device': loop_device,
'type': ['squashfs'],
'options': ['ro', 'noexec']},
'/opt/google/containers/arc-obb-mounter/mountpoints/container-root': {
'device': loop_device,
'type': ['squashfs'],
'options': ['ro', 'noexec']},
'/run/arc/adbd': {
'type': ['tmpfs'],
'options': standard_rw_options + ['mode=770']},
'/run/arc/debugfs/sync': {
'type': ['debugfs'],
'options': standard_rw_options },
'/run/arc/debugfs/tracing': { # for devices in dev mode.
'type': ['debugfs', 'tracefs'],
'options': standard_rw_options },
'/run/arc/media': {
'type': ['tmpfs'] ,
'options': standard_ro_options + ['mode=755']},
'/run/arc/obb': {
'type': ['tmpfs'],
'options': standard_ro_options + ['mode=755']},
'/run/arc/oem': {
'type': ['tmpfs'],
'options': standard_rw_options + ['mode=755']},
'/run/arc/sdcard': {
'type': ['tmpfs'],
'options': standard_ro_options + ['mode=755']},
'/run/arc/shared_mounts': {
'type': ['tmpfs'],
'options': standard_rw_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},
'/run/imageloader/PepperFlashPlayer': {
'type': ['squashfs'],
'options': ['ro', 'nodev', 'nosuid']},
'/sys': {'type': ['sysfs'], 'options': standard_rw_options},
# TODO: /run is already a tmpfs mountpoint, so ip should probably
# be modified to detect that, and not create a second mountpoint.
# crbug.com/757953
'/run/netns': {'type': ['tmpfs'], '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/cgroup/schedtune': {
'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/config': {
'type': ['configfs'],
'options': standard_rw_options},
'/sys/kernel/debug': {
'type': ['debugfs'],
'options': standard_rw_options},
'/sys/kernel/debug/tracing': {
'type': ['tracefs'],
'options': standard_rw_options},
'/sys/kernel/security': {
'type': ['securityfs'],
'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},
'/run/imageloader': {
'type': ['tmpfs'],
'options': standard_rw_options},
}
# /var/run and /var/lock are bind mounts of /run and /run/lock,
# respectively. Duplicate the entries accordingly.
expected_mount_options['/var/run'] = expected_mount_options['/run']
expected_mount_options['/var/lock'] = expected_mount_options['/run/lock']
# Base for of {/run,/etc}/daemon-store/<daemon-name> directories.
run_daemon_store_base = "/run/daemon-store/"
etc_daemon_store_base = "/etc/daemon-store/"
expected_daemon_store_mount_options = {
'type': ['tmpfs'],
'options': standard_rw_options + ['mode=755']
}
# This lists mount points that host mounts created in developer mode, after
# regular mount operations taking place during boot have completed. These
# are ignored when checking mounts present in the live system.
testmode_modded_fses = set(
['/home', '/tmp', '/usr/local', '/var/db/pkg', '/var/lib/portage'])
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
# For ARC, normalize some container paths.
fields[1] = re.sub(r'^/run/containers/android_[^/]{6}/',
r'/run/containers/android/',
fields[1])
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 or not in mtab, 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_daemon_store_directory(self, daemon_name):
"""
Check whether /run/daemon-store/<daemon-name> is root-owned and there is
a corresponding /etc/daemon-store/<daemon-name> folder.
@param daemon_name: The last part of /run/daemon-store/<daemon-name>.
Returns:
int, the number errors detected.
"""
errors = 0
run_daemon_store = self.run_daemon_store_base + daemon_name
errors += self.checkid(run_daemon_store, 0)
etc_daemon_store = self.etc_daemon_store_base + daemon_name
if not os.path.exists(etc_daemon_store):
logging.error(
'Daemon store "%s" does not have a corresponding "%s" '
'directory', run_daemon_store, etc_daemon_store)
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(['/'])
# nsfs is ignored because it's a kernel filesystem used with
# network namespaces, and is not a traditional filesystem where
# arbitrary files may be created and executed.
ignored_types = set(['ecryptfs', 'nsfs'])
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 fs in self.expected_mount_options:
mount_options = self.expected_mount_options[fs]
elif fs.startswith(self.run_daemon_store_base):
# Handle mounts inside /run/daemon-store as a special case.
daemon_name = fs[len(self.run_daemon_store_base):]
errors += self.check_daemon_store_directory(daemon_name)
mount_options = self.expected_daemon_store_mount_options
else:
logging.error(
'No expectations entry for "%s" with info "%s"',
fs, mtab[fs])
errors += 1
continue
if fs_type not in mount_options['type']:
logging.error(
'[%s] "%s" has type "%s", expected type "%s"',
mtab_path, fs, fs_type, mount_options['type'])
errors += 1
device_type = mtab[fs]['device']
if ('device' in mount_options) and (
re.match(mount_options['device'], device_type) is None):
logging.error(
'[%s] "%s" is device "%s", expected device "%s"',
mtab_path, fs, device_type, mount_options['device'])
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(mount_options['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:
errors += self.checkid(dir, 0)
# 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)