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