blob: 2d02247974e30c0e7e8a7fb77533537e13c293e2 [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, os
from autotest_lib.client.bin import test, utils
from autotest_lib.client.common_lib import error
import tempfile
class security_ModuleLocking(test.test):
"""
Handle examining the system for specific module loading capabilities.
"""
version = 1
def _passed(self, msg):
logging.info('ok: %s', msg)
def _failed(self, msg):
logging.error('FAIL: %s', msg)
self._failures.append(msg)
def _fatal(self, msg):
logging.error('FATAL: %s', msg)
raise error.TestError(msg)
def check(self, boolean, msg, fatal=False):
"""
Check boolean state and report condition to log.
@param boolean: condition to examine
@param msg: what the condition is testing
@param fatal: should the test full abort on the condition failing
"""
if boolean == True:
self._passed(msg)
else:
msg = "could not satisfy '%s'" % (msg)
if fatal:
self._fatal(msg)
else:
self._failed(msg)
def module_loaded(self, module):
"""
Detect if the given module is already loaded in the kernel.
@param module: name of module to check
"""
module = module.replace('-', '_')
match = "%s " % (module)
for line in open("/proc/modules"):
if line.startswith(match):
return True
return False
def rmmod(self, module):
"""
Unload a module if it is already loaded in the kernel.
@param module: name of module to unload
"""
if self.module_loaded(module):
utils.system("rmmod %s" % (module))
def modprobe(self, module):
"""
If a module is not already loaded in the kernel, load it via modprobe.
@param module: name of module to load
"""
if not self.module_loaded(module):
utils.system("modprobe %s" % (module))
def _module_path(self, module):
"""
Locate a kernel module's full filesystem path.
@param module: name of module to locate
"""
ko = utils.system_output("find /lib/modules -name '%s.ko'" % (module))
return ko.splitlines()[0]
def module_loads_outside_rootfs(self, module):
"""
Copies the given module into /tmp and tries to load it from there
using insmod directly.
@param module: name of module to test
"""
# Start from a clean slate.
self.rmmod(module)
# Make sure we can load with standard mechanisms.
self.modprobe(module)
self.rmmod(module)
# Load module directly with insmod from root filesystem.
ko = self._module_path(module)
utils.system("insmod %s" % (ko))
self.rmmod(module)
# Load module directly with insmod from /tmp.
tmp = "/tmp/%s.ko" % (module)
utils.system("cp %s %s" % (ko, tmp))
rc = utils.system("insmod %s" % (tmp), ignore_status=True)
# Clean up.
self.rmmod(module)
utils.system("rm %s" % (tmp))
if rc == 0:
return True
return False
def module_loads_old_api(self, module):
"""
Loads a module using the old blob-style kernel syscall. With
kmod, this requires compressing the module first to trigger
in-memory decompression and loading.
@param module: name of module to test
"""
# Start from a clean slate.
self.rmmod(module)
# Compress module to trigger the old API.
tmp = "/tmp/%s.ko.gz" % (module)
ko = self._module_path(module)
utils.system("gzip -c %s > %s" % (ko, tmp))
rc = utils.system("insmod %s" % (tmp), ignore_status=True)
# Clean up.
self.rmmod(module)
utils.system("rm %s" % (tmp))
if rc == 0:
return True
return False
def module_loads_after_bind_umount(self, module):
"""
Makes sure modules can still load after a bind mount of the
filesystem is umounted.
@param module: name of module to test
"""
# Start from a clean slate.
self.rmmod(module)
# Make sure we can load with standard mechanisms.
self.modprobe(module)
self.rmmod(module)
# Create and umount a bind mount of the root filesystem.
bind = tempfile.mkdtemp(prefix=module)
rc = utils.system("mount -o bind / %s && umount %s" % (bind, bind))
utils.system("rmdir %s" % (bind))
# Attempt to load again.
self.modprobe(module)
self.rmmod(module)
if rc == 0:
return True
return False
def run_once(self):
"""
Check that the fd-based module loading syscall is enforcing the
module fd origin to the root filesystem, and that it can be
disabled and will allow the old syscall API as well.
TODO(keescook): add production test to make sure that on a verified
boot, "/proc/sys/kernel/chromiumos/module_locking" does not exist.
"""
# Empty failure list means test passes.
self._failures = []
# Check that the sysctl is either missing or set to 1.
sysctl = "/proc/sys/kernel/chromiumos/module_locking"
if os.path.exists(sysctl):
self.check(open(sysctl).read() == '1\n', "%s enabled" % (sysctl))
# Check the enforced state is to deny non-rootfs module loads.
module = "test_module"
loaded = self.module_loads_outside_rootfs(module)
self.check(loaded == False, "cannot load %s from /tmp" % (module))
# Check old API fails when enforcement enabled.
loaded = self.module_loads_old_api(module)
self.check(loaded == False, "cannot load %s with old API" % (module))
# Make sure the bind umount bug is not present.
loaded = self.module_loads_after_bind_umount(module)
self.check(loaded == True, "can load %s after bind umount" % (module))
# If the sysctl exists, verify that it will disable the restriction.
if os.path.exists(sysctl):
# Disable restriction.
open(sysctl, "w").write("0\n")
self.check(open(sysctl).read() == '0\n', "%s disabled" % (sysctl))
# Check enforcement is disabled.
loaded = self.module_loads_outside_rootfs(module)
self.check(loaded == True, "can load %s from /tmp" % (module))
# Check old API works when enforcement disabled.
loaded = self.module_loads_old_api(module)
self.check(loaded == True, "can load %s with old API" % (module))
# Clean up.
open(sysctl, "w").write("1\n")
self.check(open(sysctl).read() == '1\n', "%s enabled" % (sysctl))
# Raise a failure if anything unexpected was seen.
if len(self._failures):
raise error.TestFail((", ".join(self._failures)))