blob: 9c05cd73f9aefe7607d2e08b7016a9566a75b3e6 [file] [log] [blame]
# 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 pwd
import re
from autotest_lib.client.bin import test, utils
from autotest_lib.client.common_lib import error
class security_StatefulPermissions(test.test):
Report all unexpected writable paths in the /mnt/stateful_partition
version = 1
_STATEFUL_ROOT = "/mnt/stateful_partition"
_AID_SYSTEM = 1000
_AID_CACHE = 2001
# Note that chronos permissions in /home are covered in greater detail
# by 'security_ProfilePermissions'.
_masks_byuser = {"adm": [],
"android-root": ["/encrypted/var/log/android.kmsg"],
"attestation": ["/unencrypted/preserve/attestation.epb"],
"avfs": [],
"bin": [],
"bluetooth": ["/encrypted/var/lib/bluetooth"],
"chaps": ["/encrypted/var/lib/chaps"],
"chronos": ["/encrypted/chronos",
"chronos-access": [],
"cras": [],
"cros-disks": [],
"cups": ["/encrypted/var/cache/cups",
"daemon": [],
"debugd": [],
"dhcp": ["/encrypted/var/lib/dhcpcd"],
"dlm": ["/encrypted/var/log/displaylink"],
"input": [],
"ipsec": [],
"lp": [],
"messagebus": [],
"mtp": [],
"news": [],
"nobody": [],
"ntfs-3g": [],
"openvpn": [],
"portage": ["/encrypted/var/log/emerge.log"],
"power": ["/encrypted/var/lib/power_manager",
"pkcs11": [],
"root": None,
"sshd": [],
"syslog": ["/encrypted/var/log"],
"tcpdump": [],
"tlsdate": [],
"tss": ["/var/lib/tpm"],
"uucp": [],
"wpa": [],
"xorg": ["/encrypted/var/lib/xkb",
def systemwide_exclusions(self):
"""Returns a list of paths that are only present on test images
and therefore should be excluded from all 'find' commands.
paths = []
# 'preserve/log' is test-only.
# Cover up Portage noise.
# Cover up Autotest noise.
return paths
def generate_prune_arguments_android(self):
"""Returns a command-line fragment to make 'find' exclude
android-data/cache: uid=AID_SYSTEM, gid=AID_CACHE
android-data/data: uid=AID_SYSTEM, gid=AID_SYSTEM
Files under these paths are created by various android users.
aroot_uid = pwd.getpwnam('android-root').pw_uid
except KeyError:
# android-root not found, so don't prune anything
return ""
cmd = "-regextype posix-extended -regex STATEFUL_ROOT/home/.shadow/"
cmd += "[[:alnum:]]{40}/vault/root/[^/]*/[^/]* "
cmd += "-uid {0} \\( -gid {1} -o -gid {2} \\) -prune -o ".format(
aroot_uid + self._AID_SYSTEM,
aroot_uid + self._AID_SYSTEM, aroot_uid + self._AID_CACHE)
return cmd
def generate_prune_arguments(self, prunelist):
"""Returns a command-line fragment to make 'find' exclude the entries
in |prunelist|.
@param prunelist: list of paths to ignore
fragment = "-path STATEFUL_ROOT%s -prune -o"
fragments = [fragment % path for path in prunelist]
return " ".join(fragments)
def generate_find(self, user, prunelist):
Generates the "find" command that spits out all files in stateful
writable by a given user, with the given list of directories removed.
@param user: report writable paths owned by this user
@param prunelist: list of paths to ignore
if prunelist is None:
return "true" # return a no-op shell command, e.g. for root.
# Exclude world-writeable stuff.
# '/var/lib/metrics/uma-events' is world-writeable:
# '/run/lock' is world-writeable.
# '/var/log/asan' should be world-writeable:
# Add system-wide exclusions.
cmd = "find STATEFUL_ROOT "
cmd += self.generate_prune_arguments(prunelist)
# Note that we don't "prune" all of /var/tmp's contents, just mask
# the dir itself. Any contents are still interesting.
cmd += " -path STATEFUL_ROOT/encrypted/var/tmp -o "
cmd += " -writable -ls -o -user %s -ls 2>/dev/null" % user
return cmd
def expected_owners(self):
"""Returns the set of file/directory owners expected in stateful."""
# In other words, this is basically the users mentioned in
# tests_byuser, except for any expected to actually own zero files.
# Currently, there's no exclusions.
return set(self._masks_byuser.keys())
def observed_owners(self):
"""Returns the set of file/directory owners present in stateful."""
cmd = "find STATEFUL_ROOT "
cmd += self.generate_prune_arguments_android()
cmd += self.generate_prune_arguments(self.systemwide_exclusions())
cmd += " -printf '%u\\n' | sort -u"
return set(self.subst_run(cmd).splitlines())
def owners_lacking_coverage(self):
Determines the set of owners not covered by any of the
per-owner tests implemented in this class. Returns
a set of usernames (possibly the empty set).
return self.observed_owners().difference(self.expected_owners())
def log_owned_files(self, owner):
Sends information about all files in the stateful partition
owned by a given owner to the standard logging facility.
@param owner: paths owned by this user will be reported
cmd = "find STATEFUL_ROOT -user %s -ls" % owner
cmd_output = self.subst_run(cmd)
def subst_run(self, cmd, stateful_root=_STATEFUL_ROOT):
Replace "STATEFUL_ROOT" with the actual stateful partition path.
@param cmd: string containing the command to examine
@param stateful_root: path used to replace "STATEFUL_ROOT"
cmd = cmd.replace("STATEFUL_ROOT", stateful_root)
return utils.system_output(cmd, ignore_status=True)
def run_once(self):
Accounts for the contents of the stateful partition
piece-wise, inspecting the level of access which can
be obtained by each of the privilege levels (usernames)
used in CrOS.
The test passes if each of the owner-specific sub-tests pass,
and if there are no files unaccounted for (i.e., no unexpected
file-owners for which we have no tests.)
testfail = False
unexpected_owners = self.owners_lacking_coverage()
if unexpected_owners:
testfail = True
for o in unexpected_owners:
# Now run the sub-tests.
for user, mask in self._masks_byuser.items():
cmd = self.generate_find(user, mask)
except KeyError, err:
logging.warning('Skipping missing user: %s', err)
# The 'EOF' below helps us distinguish 2 types of failures.
# We have to use ignore_status=True because many of these
# find-based tests will exit(nonzero) to signal that they
# encountered inaccessible directories, which we expect by-design.
# This creates ambiguity as to whether empty output means
# the test ran, and passed, or the su failed. Including an
# expected 'EOF' output disambiguates these cases.
cmd = "su -s /bin/sh -c '%s;echo EOF' %s" % (cmd, user)
cmd_output = self.subst_run(cmd)
if not cmd_output:
# we never got 'EOF', su failed
testfail = True
logging.error("su failed while attempting to run:")
logging.error("[Got %s]", cmd_output)
elif not"^\s*EOF\s*$", cmd_output):
# we got test failures before 'EOF'
testfail = True
logging.error("Test for '%s' found unexpected files:\n%s",
user, cmd_output)
if testfail:
raise error.TestFail("Unexpected files/perms in stateful")