blob: 0b6f863d7fde8208ecc05d7258c25d3d1597d8a8 [file] [log] [blame]
# Copyright 2018 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 posix
import tempfile
from autotest_lib.client.bin import test
from autotest_lib.client.common_lib import error
class security_StatefulPartitionHardening(test.test):
"""
Creates symlinks/FIFOs in various locations and attempts to access them,
checking that observed behavior matches the expected inode security policy
configured in chromeos_startup.
"""
version = 1
_BLOCKED_LOCATIONS = [
'/mnt/stateful_partition',
'/var',
]
_ALLOWED_LOCATIONS = [
'/tmp',
]
_SYMLINK_EXCEPTIONS = [
'/home',
'/var/cache/echo',
'/var/cache/vpd',
'/var/lib/timezone',
'/var/log',
]
def __init__(self, *args, **kwargs):
super(security_StatefulPartitionHardening,
self).__init__(*args, **kwargs)
self._links_created = []
self._dirs_created = []
self._failure = False
def cleanup(self):
"""
Removes symlinks and paths created during test execution after testing.
"""
for path in reversed(self._links_created):
os.unlink(path)
for path in reversed(self._dirs_created):
os.rmdir(path)
super(security_StatefulPartitionHardening, self).cleanup()
def _fail(self, msg):
"""
Log failure message and record failure.
@param msg: String to log.
"""
logging.error(msg)
self._failure = True
def _can_open(self, location):
"""
Attempt to open symlink.
@param location: Path to the symlink for opening.
@returns True if the path can be opened, False otherwise.
"""
try:
open(location, 'a+').close()
return True
except IOError:
return False
def _can_open_fifo(self, location):
"""
Attempt to open FIFO.
@param location: Path to the FIFO for opening.
@returns True if the path can be opened, False otherwise.
"""
try:
fifo = posix.open(location, posix.O_NONBLOCK)
posix.close(fifo)
return True
except IOError:
return False
def _check_open_succeeds(self, location):
"""
Attempt to open symlink and log if failure.
@param location: Path to the symlink for opening.
"""
if not self._can_open(location):
self._fail('Path access failed unexpectedly on %s' % location)
def _check_open_fifo_succeeds(self, location):
"""
Attempt to open symlink and log if failure.
@param location: Path to the symlink for opening.
"""
if not self._can_open_fifo(location):
self._fail('Path access failed unexpectedly on %s' % location)
def _check_open_fails(self, location):
"""
Attempt to open path and log if success.
@param location: Path to the file for opening.
"""
if self._can_open(location):
self._fail('Path access succeeded unexpectedly on %s' % location)
def _assert_realpath(self, path, expected):
"""
Make sure path matches expected path when symlinks are resolved.
@param path: Path for which to resolve symlinks.
@param expected: Expected path.
@raises TestError if there is an unexpected symlink in the path.
"""
resolved = os.path.realpath(path)
if resolved != expected:
raise error.TestError('Bad canonical path for %s: %s vs %s' %
(path, resolved, expected))
def _mkdir(self, location):
"""
Make directory at location.
@param location: Path to directory to be created.
"""
dir_name = tempfile.mktemp('', 'security_RootfsStatefulHardening-',
location)
os.mkdir(dir_name)
self._dirs_created.append(dir_name)
return dir_name
def _symlink(self, location, target):
"""
Create symlink from location dir to target and add it to symlink list.
@param location: Dir in which to create temporary symlink file.
@param target: Target of the symlink.
@returns the name of the temporary symlink file.
"""
link_name = tempfile.mktemp('', 'security_RootfsStatefulHardening-',
location)
os.symlink(target, link_name)
self._links_created.append(link_name)
return link_name
def _fifo(self, location):
"""
Create FIFO in location dir and add it to FIFO list.
@param location: Dir in which to create temporary FIFO.
@returns the name of the temporary FIFO.
"""
fifo_name = tempfile.mktemp('', 'security_RootfsStatefulHardening-',
location)
os.mkfifo(fifo_name)
self._links_created.append(fifo_name)
return fifo_name
def _test_blocked_traversal(self, path, canonical):
"""
Test that symlink traversal is blocked for given path.
@param path: Path to symlink. Access should fail.
@param canonical: Path to target. Access should succeed.
"""
self._assert_realpath(path, canonical)
self._check_open_succeeds(canonical)
self._check_open_fails(path)
def _test_blocked_traversal_simple(self, location):
"""
Test that symlink traversal is blocked in the simplest case.
@param location: Path to the symlink file.
"""
name = self._symlink(location, '/dev/null')
self._test_blocked_traversal(name, '/dev/null')
def _test_blocked_traversal_parent(self, location):
"""
Test that traversal is blocked when symlink is in path.
@param location: Path to the symlink to be traversed.
"""
name = self._symlink(location, '/dev')
self._test_blocked_traversal(os.path.join(name, 'null'), '/dev/null')
def _test_allowed_traversal(self, location):
"""
Test that symlink traversal is allowed.
@param location: Path to the symlink to be traversed.
"""
name = self._symlink(location, '/dev/null')
self._check_open_succeeds(name)
def _test_blocked_fifo(self, location):
"""
Test that FIFO access is blocked.
@param location: Path to the FIFO to attempt to open.
"""
fifo = self._fifo(location)
self._check_open_fails(fifo)
def _test_allowed_fifo(self, location):
"""
Test that FIFO access is allowed.
@param location: Path to the FIFO to attempt to open.
"""
fifo = self._fifo(location)
self._check_open_fifo_succeeds(fifo)
def _test_symlink_traversal(self, location, access_allowed):
"""
Test symlink traversal for given location.
@param location: Path to the symlink to be traversed.
@param access_allowed: Boolean regarding expected success of traversal.
"""
if access_allowed:
self._test_allowed_traversal(location)
self._test_allowed_traversal(self._mkdir(location))
else:
self._test_blocked_traversal_simple(location)
self._test_blocked_traversal_simple(self._mkdir(location))
self._test_blocked_traversal_parent(location)
def _test_fifo_open(self, location, access_allowed):
"""
Test FIFO opening for given location.
@param location: Path to the FIFO to be opened.
@param access_allowed: Boolean regarding expected success of FIFO open.
"""
if access_allowed:
self._test_allowed_fifo(location)
self._test_allowed_fifo(self._mkdir(location))
else:
self._test_blocked_fifo(location)
self._test_blocked_fifo(self._mkdir(location))
def run_once(self):
"""
Runs the test, creating symlinks/FIFOs and checking access behavior.
"""
# Test blocked access in blocked locations and their subdirs.
for location in self._BLOCKED_LOCATIONS:
self._test_symlink_traversal(location, False)
self._test_fifo_open(location, False)
# Test access allowed in allowed locations.
for location in self._ALLOWED_LOCATIONS:
self._test_symlink_traversal(location, True)
self._test_fifo_open(location, True)
# Test symlink traversal allowed in exempted locations and their
# subdirs.
for location in self._SYMLINK_EXCEPTIONS:
self._test_symlink_traversal(location, True)
# Test a more complicated case where the blocked symlink isn't
# actually present as a component in the accessed path, but gets
# introduced indirectly by an allowed symlink.
blocked = self._symlink(self._BLOCKED_LOCATIONS[0], '/dev/null')
allowed = self._symlink(self._SYMLINK_EXCEPTIONS[0], blocked)
self._test_blocked_traversal(allowed, '/dev/null')
# Make the test fail if any unexpected behaviour got detected. Note that
# the error log output that will be included in the failure message
# mentions the failed location to aid debugging.
if self._failure:
raise error.TestFail('Unexpected symlink/FIFO access behavior')