• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@chromium.org (Kelly Lucas)'
8
9import logging
10import os
11import re
12import shutil
13import stat
14
15from autotest_lib.client.bin import test
16from autotest_lib.client.common_lib import error
17
18class 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