1# Copyright (c) 2012 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 re 8import stat 9import subprocess 10 11from autotest_lib.client.bin import test 12from autotest_lib.client.common_lib import error 13from autotest_lib.client.cros import asan 14 15class security_OpenFDs(test.test): 16 """Checks a number of sensitive processes on Chrome OS for unexpected open 17 file descriptors. 18 """ 19 20 version = 1 21 22 @staticmethod 23 def _S_ISANONFD(mode): 24 """ 25 Returns whether |mode| represents an "anonymous inode" file descriptor. 26 Python does not expose a type-checking macro for anonymous inode fds. 27 Implements the same interface as stat.S_ISREG(x). 28 29 @param mode: mode bits, usually from 'stat(path).st_mode' 30 """ 31 return 0 == (mode & 0770000) 32 33 34 def get_fds(self, pid, typechecker): 35 """ 36 Returns the set of open file descriptors for |pid|. 37 Each open fd is represented as 'mode path', e.g.: '0500 /dev/random'. 38 39 @param pid: pid of process 40 @param typechecker: callback restricting allowed fd types 41 """ 42 proc_fd_dir = os.path.join('/proc/', pid, 'fd') 43 fd_perms = set([]) 44 for link in os.listdir(proc_fd_dir): 45 link_path = os.path.join(proc_fd_dir, link) 46 target = os.readlink(link_path) 47 # The "mode" on the link tells us if the file is 48 # opened for read/write. We are more interested 49 # in that than the permissions of the file on the fs. 50 link_st_mode = os.lstat(link_path).st_mode 51 # On the other hand, we need the type information 52 # off the real file, otherwise we're going to get 53 # S_ISLNK for everything. 54 real_st_mode = os.stat(link_path).st_mode 55 if not typechecker(real_st_mode): 56 raise error.TestFail('Pid %s has fd for %s, disallowed type' % 57 (pid, target)) 58 mode = stat.S_IMODE(link_st_mode) 59 fd_perms.add('%s %s' % (oct(mode), target)) 60 return fd_perms 61 62 63 def snapshot_system(self): 64 """ 65 Dumps a systemwide snapshot of open-fd and process table 66 information into the results directory, to assist with any 67 triage/debug later. 68 """ 69 subprocess.call('ps -ef > "%s/ps-ef.txt"' % self.resultsdir, 70 shell=True) 71 subprocess.call('ls -l /proc/*[0-9]*/fd > "%s/proc-fd.txt"' % 72 self.resultsdir, shell=True) 73 74 75 def apply_filters(self, fds, filters): 76 """ 77 Removes every item in |fds| matching any of the regexes in |filters|. 78 This modifies the set in-place, and returns a list containing 79 any regexes which did not match anything. 80 81 @param fds: set of 'mode path' strings representing open fds 82 @param filters: list of regexes to filter open fds with 83 """ 84 failed_filters = set() 85 for filter_re in filters: 86 expected_fds = set([fd_perm for fd_perm in fds 87 if re.match(filter_re, fd_perm)]) 88 if not expected_fds: 89 failed_filters.add(filter_re) 90 fds -= expected_fds 91 return failed_filters 92 93 94 def find_pids(self, process, arg_regex): 95 """ 96 Finds all pids for |process| whose command line arguments 97 match |arg_regex|. Returns a list of pids. 98 99 @param process: process name 100 @param arg_regex: regex to match command line arguments 101 """ 102 p1 = subprocess.Popen(['ps', '-C', process, '-o', 'pid,command'], 103 stdout=subprocess.PIPE) 104 # We're adding '--ignored= --type=renderer' to the GPU process cmdline 105 # to fix crbug.com/129884. 106 # This process has different characteristics, so we need to avoid 107 # finding it when we find --type=renderer tests processes. 108 p2 = subprocess.Popen(['grep', '-v', '--', 109 '--ignored=.*%s' % arg_regex], 110 stdin=p1.stdout, stdout=subprocess.PIPE) 111 p3 = subprocess.Popen(['grep', arg_regex], stdin=p2.stdout, 112 stdout=subprocess.PIPE) 113 p4 = subprocess.Popen(['awk', '{print $1}'], stdin=p3.stdout, 114 stdout=subprocess.PIPE) 115 output = p4.communicate()[0] 116 return output.splitlines() 117 118 119 def check_process(self, process, args, filters, typechecker): 120 """ 121 Checks a process for unexpected open file descriptors: 122 * Identifies all instances (pids) of |process|. 123 * Identifies all file descriptors open by those pids. 124 * Reports any fds not accounted for by regexes in |filters|. 125 * Reports any filters which fail to match any open fds. 126 127 If there were any fds unaccounted for, or failed filters, 128 mark the test failed. 129 130 @param process: process name 131 @param args: regex to match command line arguments 132 @param filters: list of regexes to filter open fds with 133 @param typechecker: callback restricting allowed fd types 134 """ 135 logging.debug('Checking %s %s', process, args) 136 test_pass = True 137 for pid in self.find_pids(process, args): 138 logging.debug('Found pid %s for %s', pid, process) 139 fds = self.get_fds(pid, typechecker) 140 failed_filters = self.apply_filters(fds, filters) 141 # Log failed filters to allow pruning the list. 142 if failed_filters: 143 logging.error('Some filter(s) failed to match any fds: %s', 144 repr(failed_filters)) 145 if fds: 146 logging.error('Found unexpected fds in %s %s: %s', 147 process, args, repr(fds)) 148 test_pass = False 149 return test_pass 150 151 152 def run_once(self): 153 """ 154 Checks a number of sensitive processes on Chrome OS for 155 unexpected open file descriptors. 156 """ 157 self.snapshot_system() 158 159 passes = [] 160 filters = [r'0700 anon_inode:\[event.*\]', 161 r'0[35]00 pipe:.*', 162 r'0[57]00 socket:.*', 163 r'0500 /dev/null', 164 r'0[57]00 /dev/urandom', 165 r'0300 /var/log/chrome/chrome_.*', 166 r'0[37]00 /var/log/ui/ui.*', 167 ] 168 169 # Whitelist fd-type check, suitable for Chrome processes. 170 # Notably, this omits S_ISDIR. 171 allowed_fd_type_check = lambda x: (stat.S_ISREG(x) or 172 stat.S_ISCHR(x) or 173 stat.S_ISSOCK(x) or 174 stat.S_ISFIFO(x) or 175 security_OpenFDs._S_ISANONFD(x)) 176 177 # TODO(jorgelo): revisit this and potentially remove. 178 if asan.running_on_asan(): 179 # On ASan, allow all fd types and opening /proc 180 logging.info("Running on ASan, allowing /proc") 181 allowed_fd_type_check = lambda x: True 182 filters.append(r'0500 /proc') 183 184 passes.append(self.check_process('chrome', 'type=plugin', filters, 185 allowed_fd_type_check)) 186 187 filters.extend([r'0[57]00 /dev/shm/..*', 188 r'0500 /opt/google/chrome/.*.pak', 189 r'0500 /opt/google/chrome/icudtl.dat', 190 # These used to be bundled with the Chrome binary. 191 # See crbug.com/475170. 192 r'0500 /opt/google/chrome/natives_blob.bin', 193 r'0500 /opt/google/chrome/snapshot_blob.bin', 194 # Font files can be kept open in renderers 195 # for performance reasons. 196 # See crbug.com/452227. 197 r'0500 /usr/share/fonts/.*', 198 ]) 199 try: 200 # Renderers have access to DRM vgem device for graphics tile upload. 201 # See crbug.com/537474. 202 filters.append(r'0700 /dev/dri/%s' % os.readlink('/dev/dri/vgem')) 203 except OSError: 204 # /dev/dri/vgem doesn't exist. 205 pass 206 207 passes.append(self.check_process('chrome', 'type=renderer', filters, 208 allowed_fd_type_check)) 209 210 if False in passes: 211 raise error.TestFail("Unexpected open file descriptors.") 212