# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import os import re import stat import subprocess from autotest_lib.client.bin import test from autotest_lib.client.common_lib import error from autotest_lib.client.cros import asan class security_OpenFDs(test.test): """Checks a number of sensitive processes on Chrome OS for unexpected open file descriptors. """ version = 1 @staticmethod def _S_ISANONFD(mode): """ Returns whether |mode| represents an "anonymous inode" file descriptor. Python does not expose a type-checking macro for anonymous inode fds. Implements the same interface as stat.S_ISREG(x). @param mode: mode bits, usually from 'stat(path).st_mode' """ return 0 == (mode & 0770000) def get_fds(self, pid, typechecker): """ Returns the set of open file descriptors for |pid|. Each open fd is represented as 'mode path', e.g.: '0500 /dev/random'. @param pid: pid of process @param typechecker: callback restricting allowed fd types """ proc_fd_dir = os.path.join('/proc/', pid, 'fd') fd_perms = set([]) for link in os.listdir(proc_fd_dir): link_path = os.path.join(proc_fd_dir, link) target = os.readlink(link_path) # The "mode" on the link tells us if the file is # opened for read/write. We are more interested # in that than the permissions of the file on the fs. link_st_mode = os.lstat(link_path).st_mode # On the other hand, we need the type information # off the real file, otherwise we're going to get # S_ISLNK for everything. real_st_mode = os.stat(link_path).st_mode if not typechecker(real_st_mode): raise error.TestFail('Pid %s has fd for %s, disallowed type' % (pid, target)) mode = stat.S_IMODE(link_st_mode) fd_perms.add('%s %s' % (oct(mode), target)) return fd_perms def snapshot_system(self): """ Dumps a systemwide snapshot of open-fd and process table information into the results directory, to assist with any triage/debug later. """ subprocess.call('ps -ef > "%s/ps-ef.txt"' % self.resultsdir, shell=True) subprocess.call('ls -l /proc/*[0-9]*/fd > "%s/proc-fd.txt"' % self.resultsdir, shell=True) def apply_filters(self, fds, filters): """ Removes every item in |fds| matching any of the regexes in |filters|. This modifies the set in-place, and returns a list containing any regexes which did not match anything. @param fds: set of 'mode path' strings representing open fds @param filters: list of regexes to filter open fds with """ failed_filters = set() for filter_re in filters: expected_fds = set([fd_perm for fd_perm in fds if re.match(filter_re, fd_perm)]) if not expected_fds: failed_filters.add(filter_re) fds -= expected_fds return failed_filters def find_pids(self, process, arg_regex): """ Finds all pids for |process| whose command line arguments match |arg_regex|. Returns a list of pids. @param process: process name @param arg_regex: regex to match command line arguments """ p1 = subprocess.Popen(['ps', '-C', process, '-o', 'pid,command'], stdout=subprocess.PIPE) # We're adding '--ignored= --type=renderer' to the GPU process cmdline # to fix crbug.com/129884. # This process has different characteristics, so we need to avoid # finding it when we find --type=renderer tests processes. p2 = subprocess.Popen(['grep', '-v', '--', '--ignored=.*%s' % arg_regex], stdin=p1.stdout, stdout=subprocess.PIPE) p3 = subprocess.Popen(['grep', arg_regex], stdin=p2.stdout, stdout=subprocess.PIPE) p4 = subprocess.Popen(['awk', '{print $1}'], stdin=p3.stdout, stdout=subprocess.PIPE) output = p4.communicate()[0] return output.splitlines() def check_process(self, process, args, filters, typechecker): """ Checks a process for unexpected open file descriptors: * Identifies all instances (pids) of |process|. * Identifies all file descriptors open by those pids. * Reports any fds not accounted for by regexes in |filters|. * Reports any filters which fail to match any open fds. If there were any fds unaccounted for, or failed filters, mark the test failed. @param process: process name @param args: regex to match command line arguments @param filters: list of regexes to filter open fds with @param typechecker: callback restricting allowed fd types """ logging.debug('Checking %s %s', process, args) test_pass = True for pid in self.find_pids(process, args): logging.debug('Found pid %s for %s', pid, process) fds = self.get_fds(pid, typechecker) failed_filters = self.apply_filters(fds, filters) # Log failed filters to allow pruning the list. if failed_filters: logging.error('Some filter(s) failed to match any fds: %s', repr(failed_filters)) if fds: logging.error('Found unexpected fds in %s %s: %s', process, args, repr(fds)) test_pass = False return test_pass def run_once(self): """ Checks a number of sensitive processes on Chrome OS for unexpected open file descriptors. """ self.snapshot_system() passes = [] filters = [r'0700 anon_inode:\[event.*\]', r'0[35]00 pipe:.*', r'0[57]00 socket:.*', r'0500 /dev/null', r'0[57]00 /dev/urandom', r'0300 /var/log/chrome/chrome_.*', r'0[37]00 /var/log/ui/ui.*', ] # Whitelist fd-type check, suitable for Chrome processes. # Notably, this omits S_ISDIR. allowed_fd_type_check = lambda x: (stat.S_ISREG(x) or stat.S_ISCHR(x) or stat.S_ISSOCK(x) or stat.S_ISFIFO(x) or security_OpenFDs._S_ISANONFD(x)) # TODO(jorgelo): revisit this and potentially remove. if asan.running_on_asan(): # On ASan, allow all fd types and opening /proc logging.info("Running on ASan, allowing /proc") allowed_fd_type_check = lambda x: True filters.append(r'0500 /proc') passes.append(self.check_process('chrome', 'type=plugin', filters, allowed_fd_type_check)) filters.extend([r'0[57]00 /dev/shm/..*', r'0500 /opt/google/chrome/.*.pak', r'0500 /opt/google/chrome/icudtl.dat', # These used to be bundled with the Chrome binary. # See crbug.com/475170. r'0500 /opt/google/chrome/natives_blob.bin', r'0500 /opt/google/chrome/snapshot_blob.bin', # Font files can be kept open in renderers # for performance reasons. # See crbug.com/452227. r'0500 /usr/share/fonts/.*', # Zero-copy texture uploads. crbug.com/607632. r'0700 anon_inode:dmabuf', # Ad blocking ruleset mmapped in for performance. r'0500 /home/chronos/Subresource Filter/Indexed Rules/[0-9]*/[0-9\.]*/Ruleset Data' ]) try: # Renderers have access to DRM vgem device for graphics tile upload. # See crbug.com/537474. filters.append(r'0700 /dev/dri/%s' % os.readlink('/dev/dri/vgem')) except OSError: # /dev/dri/vgem doesn't exist. pass passes.append(self.check_process('chrome', 'type=renderer', filters, allowed_fd_type_check)) if False in passes: raise error.TestFail("Unexpected open file descriptors.")