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