Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 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 | |
| 5 | import logging |
| 6 | import os |
| 7 | import re |
| 8 | import stat |
| 9 | import subprocess |
| 10 | |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 11 | from autotest_lib.client.bin import test |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 12 | from autotest_lib.client.common_lib import error |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 13 | from autotest_lib.client.cros import asan |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 14 | |
| 15 | class security_OpenFDs(test.test): |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 16 | """Checks a number of sensitive processes on Chrome OS for unexpected open |
| 17 | file descriptors. |
| 18 | """ |
| 19 | |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 20 | version = 1 |
| 21 | |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 22 | @staticmethod |
| 23 | def _S_ISANONFD(mode): |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 24 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 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). |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 28 | |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 29 | @param mode: mode bits, usually from 'stat(path).st_mode' |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 30 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 31 | return 0 == (mode & 0770000) |
Jorge Lucangeli Obes | 3ceb659 | 2014-12-09 09:52:00 -0800 | [diff] [blame] | 32 | |
| 33 | |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 34 | def get_fds(self, pid, typechecker): |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 35 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 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 |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 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) |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 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) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 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 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 77 | Removes every item in |fds| matching any of the regexes in |filters|. |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 78 | This modifies the set in-place, and returns a list containing |
| 79 | any regexes which did not match anything. |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 80 | |
| 81 | @param fds: set of 'mode path' strings representing open fds |
| 82 | @param filters: list of regexes to filter open fds with |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 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 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 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 |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 101 | """ |
| 102 | p1 = subprocess.Popen(['ps', '-C', process, '-o', 'pid,command'], |
| 103 | stdout=subprocess.PIPE) |
Jim Hebert | b34a060 | 2012-07-03 13:55:21 -0700 | [diff] [blame] | 104 | # We're adding '--ignored= --type=renderer' to the GPU process cmdline |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 105 | # to fix crbug.com/129884. |
Jim Hebert | b34a060 | 2012-07-03 13:55:21 -0700 | [diff] [blame] | 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, |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 112 | stdout=subprocess.PIPE) |
Jim Hebert | b34a060 | 2012-07-03 13:55:21 -0700 | [diff] [blame] | 113 | p4 = subprocess.Popen(['awk', '{print $1}'], stdin=p3.stdout, |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 114 | stdout=subprocess.PIPE) |
Jim Hebert | b34a060 | 2012-07-03 13:55:21 -0700 | [diff] [blame] | 115 | output = p4.communicate()[0] |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 116 | return output.splitlines() |
| 117 | |
| 118 | |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 119 | def check_process(self, process, args, filters, typechecker): |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 120 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 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 |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 134 | """ |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 135 | logging.debug('Checking %s %s', process, args) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 136 | test_pass = True |
| 137 | for pid in self.find_pids(process, args): |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 138 | logging.debug('Found pid %s for %s', pid, process) |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 139 | fds = self.get_fds(pid, typechecker) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 140 | failed_filters = self.apply_filters(fds, filters) |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 141 | # Log failed filters to allow pruning the list. |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 142 | if failed_filters: |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 143 | logging.error('Some filter(s) failed to match any fds: %s', |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 144 | repr(failed_filters)) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 145 | if fds: |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 146 | logging.error('Found unexpected fds in %s %s: %s', |
| 147 | process, args, repr(fds)) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 148 | test_pass = False |
| 149 | return test_pass |
| 150 | |
| 151 | |
| 152 | def run_once(self): |
| 153 | """ |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 154 | Checks a number of sensitive processes on Chrome OS for |
| 155 | unexpected open file descriptors. |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 156 | """ |
| 157 | self.snapshot_system() |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 158 | |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 159 | passes = [] |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 160 | filters = [r'0700 anon_inode:\[event.*\]', |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 161 | r'0[35]00 pipe:.*', |
| 162 | r'0[57]00 socket:.*', |
Jim Hebert | 54876d4 | 2012-12-06 16:53:36 -0800 | [diff] [blame] | 163 | r'0500 /dev/null', |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 164 | r'0[57]00 /dev/urandom', |
| 165 | r'0300 /var/log/chrome/chrome_.*', |
Jim Hebert | 54876d4 | 2012-12-06 16:53:36 -0800 | [diff] [blame] | 166 | r'0[37]00 /var/log/ui/ui.*', |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 167 | ] |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 168 | |
| 169 | # Whitelist fd-type check, suitable for Chrome processes. |
| 170 | # Notably, this omits S_ISDIR. |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 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)) |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 176 | |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 177 | # TODO(jorgelo): revisit this and potentially remove. |
| 178 | if asan.running_on_asan(): |
Jorge Lucangeli Obes | 3ceb659 | 2014-12-09 09:52:00 -0800 | [diff] [blame] | 179 | # On ASan, allow all fd types and opening /proc |
| 180 | logging.info("Running on ASan, allowing /proc") |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 181 | allowed_fd_type_check = lambda x: True |
Jorge Lucangeli Obes | 3ceb659 | 2014-12-09 09:52:00 -0800 | [diff] [blame] | 182 | filters.append(r'0500 /proc') |
| 183 | |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 184 | passes.append(self.check_process('chrome', 'type=plugin', filters, |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 185 | allowed_fd_type_check)) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 186 | |
Jorge Lucangeli Obes | 3bd4e23 | 2013-11-25 11:36:08 -0800 | [diff] [blame] | 187 | filters.extend([r'0[57]00 /dev/shm/..*', |
Jim Hebert | 23a63b0 | 2012-08-09 18:06:28 -0700 | [diff] [blame] | 188 | r'0500 /opt/google/chrome/.*.pak', |
Jorge Lucangeli Obes | 8c7b99e | 2014-02-14 14:29:09 -0800 | [diff] [blame] | 189 | r'0500 /opt/google/chrome/icudtl.dat', |
Jorge Lucangeli Obes | 24178b1 | 2015-04-08 20:22:12 -0700 | [diff] [blame] | 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', |
Jorge Lucangeli Obes | 09dd045 | 2015-09-30 16:42:06 -0700 | [diff] [blame] | 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 | # Renderers have access to GPU render nodes. |
| 199 | # See crbug.com/537474. |
Jorge Lucangeli Obes | 741fb66 | 2015-10-20 22:08:11 -0700 | [diff] [blame^] | 200 | r'0700 /dev/dri/renderD12[89]', |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 201 | ]) |
Jorge Lucangeli Obes | 741fb66 | 2015-10-20 22:08:11 -0700 | [diff] [blame^] | 202 | try: |
| 203 | filters.append(r'0700 /dev/dri/%s' % os.readlink('/dev/dri/vgem')) |
| 204 | except OSError: |
| 205 | # /dev/dri/vgem doesn't exist. |
| 206 | pass |
| 207 | |
Jim Hebert | 64ed5cc | 2012-11-06 14:08:43 -0800 | [diff] [blame] | 208 | passes.append(self.check_process('chrome', 'type=renderer', filters, |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 209 | allowed_fd_type_check)) |
Jim Hebert | 2e04d94 | 2012-05-18 11:46:02 -0700 | [diff] [blame] | 210 | |
| 211 | if False in passes: |
Jorge Lucangeli Obes | e84e154 | 2014-12-08 17:53:09 -0800 | [diff] [blame] | 212 | raise error.TestFail("Unexpected open file descriptors.") |