Home | History | Annotate | Download | only in security_HardlinkRestrictions
      1 # Copyright (c) 2011 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 #
      5 #    Based on tests from http://bazaar.launchpad.net/~ubuntu-bugcontrol/qa-regression-testing/master/view/head:/scripts/test-kernel-security.py
      6 #    Copyright (C) 2008-2011 Canonical Ltd.
      7 #
      8 #    This program is free software: you can redistribute it and/or modify
      9 #    it under the terms of the GNU General Public License version 3,
     10 #    as published by the Free Software Foundation.
     11 #
     12 #    This program is distributed in the hope that it will be useful,
     13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
     14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     15 #    GNU General Public License for more details.
     16 #
     17 #    You should have received a copy of the GNU General Public License
     18 #    along with this program. If not, see <http://www.gnu.org/licenses/>.
     19 
     20 import pwd
     21 import tempfile
     22 import shutil
     23 import logging, os
     24 from autotest_lib.client.bin import test, utils
     25 from autotest_lib.client.common_lib import error
     26 
     27 class security_HardlinkRestrictions(test.test):
     28     version = 1
     29 
     30     def _passed(self, msg):
     31         logging.info('ok: %s' % (msg))
     32 
     33     def _failed(self, msg):
     34         logging.error('FAIL: %s' % (msg))
     35         self._failures.append(msg)
     36 
     37     def _fatal(self, msg):
     38         logging.error('FATAL: %s' % (msg))
     39         raise error.TestError(msg)
     40 
     41     def check(self, boolean, msg, fatal=False):
     42         if boolean == True:
     43             self._passed(msg)
     44         else:
     45             msg = "could not satisfy '%s'" % (msg)
     46             if fatal:
     47                 self._fatal(msg)
     48             else:
     49                 self._failed(msg)
     50 
     51     def _is_readable(self, path, user, expected=True):
     52         rc = utils.system("su -c 'cat %s' %s" % (path, user),
     53                           ignore_status=True)
     54         status = (rc == 0)
     55 
     56         if status != expected:
     57             if expected:
     58                 self._failed("'%s' was unable to read file '%s'" %
     59                              (user, path))
     60             else:
     61                 self._failed("'%s' was able to read file '%s'" %
     62                              (user, path))
     63         return status
     64 
     65     def _is_writable(self, path, user, expected=True):
     66         rc = utils.system("su -c 'echo > %s' %s" % (path, user),
     67                           ignore_status=True)
     68         status = (rc == 0)
     69 
     70         if status != expected:
     71             if expected:
     72                 self._failed("'%s' was unable to write file '%s'" %
     73                              (user, path))
     74             else:
     75                 self._failed("'%s' was able to write file '%s'" %
     76                              (user, path))
     77         return status
     78 
     79     def _can_hardlink(self, source, target, user, expected=True):
     80         rc = utils.system("su -c 'ln %s %s' %s" % (source, target, user),
     81                           ignore_status=True)
     82         status = (rc == 0)
     83 
     84         if status != expected:
     85             if expected:
     86                 self._failed("'%s' was unable to hardlink file '%s' as '%s'" %
     87                              (user, source, target))
     88             else:
     89                 self._failed("'%s' was able to hardlink file '%s' as '%s'" %
     90                              (user, source, target))
     91 
     92         # Check and clean up hardlink if it was created.
     93         if os.path.exists(target):
     94             if not expected:
     95                 self._failed("'%s' was able to create hardlink '%s' to '%s'" %
     96                              (user, target, source))
     97             os.unlink(target)
     98 
     99         return status
    100 
    101     def _check_hardlinks(self, user):
    102         uid = pwd.getpwnam(user)[2]
    103 
    104         # Verify we have a distinct user.
    105         if uid == 0:
    106             self._failed("The '%s' user is root(%d)!" % (user, uid))
    107             return
    108 
    109         # Build a world-writable directory, owned by user.
    110         tmpdir = tempfile.mkdtemp(prefix='hardlinks-')
    111         self._rmdir.append(tmpdir)
    112         os.chown(tmpdir, uid, 0)
    113 
    114         # Create test target files.
    115         secret = tempfile.NamedTemporaryFile(prefix="secret-")
    116         readable = tempfile.NamedTemporaryFile(prefix="readable-")
    117         os.chmod(readable.name, 0444)
    118         available = tempfile.NamedTemporaryFile(prefix="available-")
    119         os.chmod(available.name, 0666)
    120 
    121         # Verify secret target is unreadable/unwritable.
    122         self._is_readable(secret.name, user, expected=False)
    123         self._is_writable(secret.name, user, expected=False)
    124         # Verify readable target is only readable.
    125         self._is_readable(readable.name, user)
    126         self._is_writable(readable.name, user, expected=False)
    127         # Verify available target is both readable/writable.
    128         self._is_readable(available.name, user)
    129         self._is_writable(available.name, user)
    130 
    131         # Create pathnames for hardlinks.
    132         mine = os.path.join(tmpdir, 'mine')
    133         evil = os.path.join(tmpdir, 'evil')
    134         not_evil = os.path.join(tmpdir, 'not-evil')
    135 
    136         # Allow hardlink to files owned by the user, or writable.
    137         self._is_writable(mine, user)
    138         self._can_hardlink(mine, not_evil, user)
    139         self._can_hardlink(available.name, not_evil, user)
    140 
    141         # Disallow hardlinking to unwritable or unreadlabe files.
    142         self._can_hardlink(readable.name, evil, user, expected=False)
    143         self._can_hardlink(secret.name, evil, user, expected=False)
    144 
    145         # Disallow hardlinks to unowned non-regular files. This uses
    146         # /dev because the other locations are mounted nodev, which
    147         # will cause the read/write tests to fail.
    148         devdir = tempfile.mkdtemp(prefix="hardlinks-", dir="/dev")
    149         self._rmdir.append(devdir)
    150         os.chown(devdir, uid, 0)
    151         null = os.path.join(devdir, "null")
    152         dev_evil = os.path.join(devdir, "evil")
    153         dev_not_evil = os.path.join(devdir, "not-evil")
    154         utils.system("mknod -m 0666 %s c 1 3" % (null))
    155         self._is_readable(null, user)
    156         self._is_writable(null, user)
    157         self._can_hardlink(null, dev_evil, user, expected=False)
    158 
    159         # Allow hardlinks to owned non-regular files.
    160         os.chown(null, uid, 0)
    161         self._can_hardlink(null, dev_not_evil, user)
    162 
    163         # Allow CAP_FOWNER to hardlink non-regular files.
    164         self._can_hardlink(null, dev_not_evil, "root")
    165 
    166     def run_once(self):
    167         # Empty failure list means test passes.
    168         self._failures = []
    169 
    170         # Prepare list of directories to clean up.
    171         self._rmdir = []
    172 
    173         # Verify hardlink restrictions sysctl exists and is enabled.
    174         sysctl = "/proc/sys/fs/protected_hardlinks"
    175         if (not os.path.exists(sysctl)):
    176             # Fall back to looking for Yama link restriction sysctl.
    177             sysctl = "/proc/sys/kernel/yama/protected_nonaccess_hardlinks"
    178         self.check(os.path.exists(sysctl), "%s exists" % (sysctl), fatal=True)
    179         self.check(open(sysctl).read() == '1\n', "%s enabled" % (sysctl),
    180                    fatal=True)
    181 
    182         # Test the basic "user hardlinks to unwritable source" situation
    183         # first, in a more auditable way than the extensive behavior tests
    184         # that follow.
    185         if os.path.exists("/tmp/evil-hardlink"):
    186             os.unlink("/tmp/evil-hardlink")
    187         rc = utils.system("su -c 'ln /etc/shadow /tmp/evil-hardlink' chronos",
    188                           ignore_status=True)
    189         if rc != 1 or os.path.exists("/tmp/evil-hardlink"):
    190             self._failed("chronos user was able to create malicious hardlink")
    191 
    192         # Test hardlink restrictions.
    193         self._check_hardlinks(user='chronos')
    194 
    195         # Clean up from the tests.
    196         for path in self._rmdir:
    197             if os.path.exists(path):
    198                 shutil.rmtree(path, ignore_errors=True)
    199 
    200         # Raise a failure if anything unexpected was seen.
    201         if len(self._failures):
    202             raise error.TestFail((", ".join(self._failures)))
    203