• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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