• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2018 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7import subprocess
8import shutil
9import tempfile
10
11from autotest_lib.client.bin import test, utils
12from autotest_lib.client.common_lib import error
13
14MOUNT_PATH=tempfile.mkdtemp()
15
16class security_NosymfollowMountOption(test.test):
17    """
18    Mount filesystems with the "nosymfollow" option and ensure symlink
19    traversal is blocked.
20    """
21    version = 1
22
23    def __init__(self, *args, **kwargs):
24        # TODO(mortonm): add a function to utils to do this kernel version
25        # check and raise NAError.
26        version = utils.get_kernel_version()
27        if version == "3.8.11":
28            raise error.TestNAError('Test is n/a for kernels older than 3.10')
29        super(security_NosymfollowMountOption,
30            self).__init__(*args, **kwargs)
31        self._failure = False
32
33    def cleanup(self):
34        """
35        Clean up test environment.
36        """
37        super(security_NosymfollowMountOption, self).cleanup()
38        shutil.rmtree(MOUNT_PATH)
39
40    def _fail(self, msg):
41        """
42        Log failure message and record failure.
43
44        @param msg: String to log.
45
46        """
47        logging.error(msg)
48        self._failure = True
49
50    def umount(self):
51        """
52        Unmount file system at MOUNT_PATH location.
53        """
54        try:
55            subprocess.check_output(["/bin/umount", MOUNT_PATH])
56        except subprocess.CalledProcessError, e:
57            self._fail("umount call failed")
58
59    def mount_and_test_with_string(self, mount_options, restrict_symlinks):
60        """
61        Mount file system with given options, check it was mounted with
62        correct options, and make sure symlink traversal restriction works as
63        expected.
64
65        @param mount_options: Mount options string.
66
67        @param restrict_symlinks: True if mount options should cause symlinks
68        to be restricted, False otherwise.
69
70        """
71        try:
72            subprocess.check_output(["/bin/mount",
73                                            "-n",
74                                            "-t",
75                                            "tmpfs",
76                                            "-o",
77                                            mount_options,
78                                            "tmpfs",
79                                            MOUNT_PATH])
80        except subprocess.CalledProcessError:
81            self._fail("mount call failed")
82            return
83
84        try:
85            ps = subprocess.Popen(('mount'), stdout=subprocess.PIPE)
86            output = subprocess.check_output(('grep',MOUNT_PATH),
87                                                        stdin=ps.stdout)
88            ps.wait()
89
90            for arg in mount_options.split(','):
91                if arg == "nosymfollow":
92                    continue
93                else:
94                    if output.find(arg) == -1:
95                        self._fail("filesystem missing '%s' arg" % arg)
96                        return
97
98            try:
99                open(MOUNT_PATH + "/file", "w+")
100                os.symlink(MOUNT_PATH + "/file", MOUNT_PATH + "/link")
101            except IOError:
102                self._fail("creating/linking files failed")
103                return
104
105            traversal_restricted = False
106            try:
107                open(MOUNT_PATH + "/link", "r")
108            except IOError:
109                traversal_restricted = True
110
111            if restrict_symlinks:
112                if not traversal_restricted:
113                    self._fail("symlink traversal was not restricted")
114                    return
115            else:
116                if traversal_restricted:
117                    self._fail("symlink traversal was restricted")
118        finally:
119            self.umount()
120
121    def run_once(self, test_selinux_interaction):
122        """
123        Runs the test, mounting filesystems and checking symlink traversal
124        behavior.
125        """
126        self.mount_and_test_with_string("nosymfollow", True)
127        self.mount_and_test_with_string("nodev,noexec,nosuid,nosymfollow", True)
128        self.mount_and_test_with_string("nodev,noexec,nosuid", False)
129
130        if test_selinux_interaction:
131            if not os.path.exists('/etc/selinux'):
132                raise error.TestNAError('Test is n/a if selinux is not enabled')
133            self.mount_and_test_with_string("nosymfollow,"
134                                            "context=u:object_r:tmpfs:s0,"
135                                            "fscontext=u:object_r:tmpfs:s0",
136                                            True)
137            self.mount_and_test_with_string("context=u:object_r:tmpfs:s0,"
138                                            "nosymfollow,"
139                                            "fscontext=u:object_r:tmpfs:s0",
140                                            True)
141            self.mount_and_test_with_string("context=u:object_r:tmpfs:s0,"
142                                            "fscontext=u:object_r:tmpfs:s0,"
143                                            "nosymfollow",
144                                            True)
145
146        # Make the test fail if any unexpected behaviour got detected. Note
147        # that the error log output that will be included in the failure
148        # message mentions the failed location to aid debugging.
149        if self._failure:
150            raise error.TestFail('Unexpected mount behavior')
151