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 (at] chromium.org (Kelly Lucas)' 8 9 import logging 10 import os 11 import re 12 import shutil 13 import stat 14 15 from autotest_lib.client.bin import test 16 from autotest_lib.client.common_lib import error 17 18 class 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