1 #! /usr/bin/python -Es 2 # Authors: Dan Walsh <dwalsh (at] redhat.com> 3 # Authors: Thomas Liu <tliu (at] fedoraproject.org> 4 # Authors: Josh Cogliati 5 # 6 # Copyright (C) 2009,2010 Red Hat 7 # see file 'COPYING' for use and warranty information 8 # 9 # This program is free software; you can redistribute it and/or 10 # modify it under the terms of the GNU General Public License as 11 # published by the Free Software Foundation; version 2 only 12 # 13 # This program is distributed in the hope that it will be useful, 14 # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 # GNU General Public License for more details. 17 # 18 # You should have received a copy of the GNU General Public License 19 # along with this program; if not, write to the Free Software 20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 # 22 23 import os 24 import stat 25 import sys 26 import socket 27 import random 28 import fcntl 29 import shutil 30 import re 31 import subprocess 32 import selinux 33 import signal 34 from tempfile import mkdtemp 35 import pwd 36 import sepolicy 37 38 SEUNSHARE = "/usr/sbin/seunshare" 39 SANDBOXSH = "/usr/share/sandbox/sandboxX.sh" 40 PROGNAME = "policycoreutils" 41 try: 42 import gettext 43 kwargs = {} 44 if sys.version_info < (3,): 45 kwargs['unicode'] = True 46 gettext.install(PROGNAME, 47 localedir="/usr/share/locale", 48 codeset='utf-8', 49 **kwargs) 50 except: 51 try: 52 import builtins 53 builtins.__dict__['_'] = str 54 except ImportError: 55 import __builtin__ 56 __builtin__.__dict__['_'] = unicode 57 58 DEFAULT_WINDOWSIZE = "1000x700" 59 DEFAULT_TYPE = "sandbox_t" 60 DEFAULT_X_TYPE = "sandbox_x_t" 61 SAVE_FILES = {} 62 63 random.seed(None) 64 65 66 def sighandler(signum, frame): 67 signal.signal(signum, signal.SIG_IGN) 68 os.kill(0, signum) 69 raise KeyboardInterrupt 70 71 72 def setup_sighandlers(): 73 signal.signal(signal.SIGHUP, sighandler) 74 signal.signal(signal.SIGQUIT, sighandler) 75 signal.signal(signal.SIGTERM, sighandler) 76 77 78 def error_exit(msg): 79 sys.stderr.write("%s: " % sys.argv[0]) 80 sys.stderr.write("%s\n" % msg) 81 sys.stderr.flush() 82 sys.exit(1) 83 84 85 def copyfile(file, srcdir, dest): 86 import re 87 if file.startswith(srcdir): 88 dname = os.path.dirname(file) 89 bname = os.path.basename(file) 90 if dname == srcdir: 91 dest = dest + "/" + bname 92 else: 93 newdir = re.sub(srcdir, dest, dname) 94 if not os.path.exists(newdir): 95 os.makedirs(newdir) 96 dest = newdir + "/" + bname 97 98 try: 99 if os.path.isdir(file): 100 shutil.copytree(file, dest) 101 else: 102 shutil.copy2(file, dest) 103 104 except shutil.Error as elist: 105 for e in elist.message: 106 sys.stderr.write(e[2]) 107 108 SAVE_FILES[file] = (dest, os.path.getmtime(dest)) 109 110 111 def savefile(new, orig, X_ind): 112 copy = False 113 if(X_ind): 114 import gi 115 gi.require_version('Gtk', '3.0') 116 from gi.repository import Gtk 117 dlg = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO, 118 Gtk.ButtonsType.YES_NO, 119 _("Do you want to save changes to '%s' (Y/N): ") % orig) 120 dlg.set_title(_("Sandbox Message")) 121 dlg.set_position(Gtk.WindowPosition.MOUSE) 122 dlg.show_all() 123 rc = dlg.run() 124 dlg.destroy() 125 if rc == Gtk.ResponseType.YES: 126 copy = True 127 else: 128 try: 129 input = raw_input 130 except NameError: 131 pass 132 ans = input(_("Do you want to save changes to '%s' (y/N): ") % orig) 133 if(re.match(_("[yY]"), ans)): 134 copy = True 135 if(copy): 136 shutil.copy2(new, orig) 137 138 139 def reserve(level): 140 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 141 sock.bind("\0%s" % level) 142 fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) 143 144 145 def get_range(): 146 try: 147 level = selinux.getcon_raw()[1].split(":")[4] 148 lowc, highc = level.split(".") 149 low = int(lowc[1:]) 150 high = int(highc[1:]) + 1 151 if high - low == 0: 152 raise IndexError 153 154 return low, high 155 except IndexError: 156 raise ValueError(_("User account must be setup with an MCS Range")) 157 158 159 def gen_mcs(): 160 low, high = get_range() 161 162 level = None 163 ctr = 0 164 total = high - low 165 total = (total * (total - 1)) / 2 166 while ctr < total: 167 ctr += 1 168 i1 = random.randrange(low, high) 169 i2 = random.randrange(low, high) 170 if i1 == i2: 171 continue 172 if i1 > i2: 173 tmp = i1 174 i1 = i2 175 i2 = tmp 176 level = "s0:c%d,c%d" % (i1, i2) 177 try: 178 reserve(level) 179 except socket.error: 180 continue 181 break 182 if level: 183 return level 184 raise ValueError(_("Failed to find any unused category sets. Consider a larger MCS range for this user.")) 185 186 187 def fullpath(cmd): 188 for i in ["/", "./", "../"]: 189 if cmd.startswith(i): 190 return cmd 191 for i in os.environ["PATH"].split(':'): 192 f = "%s/%s" % (i, cmd) 193 if os.access(f, os.X_OK): 194 return f 195 return cmd 196 197 198 class Sandbox: 199 SYSLOG = "/var/log/messages" 200 201 def __init__(self): 202 self.setype = DEFAULT_TYPE 203 self.__options = None 204 self.__cmds = None 205 self.__init_files = [] 206 self.__paths = [] 207 self.__mount = False 208 self.__level = None 209 self.__homedir = None 210 self.__tmpdir = None 211 212 def __validate_mount(self): 213 if self.__options.level: 214 if not self.__options.homedir or not self.__options.tmpdir: 215 self.usage(_("Homedir and tempdir required for level mounts")) 216 217 if not os.path.exists(SEUNSHARE): 218 raise ValueError(_(""" 219 %s is required for the action you want to perform. 220 """) % SEUNSHARE) 221 222 def __mount_callback(self, option, opt, value, parser): 223 self.__mount = True 224 225 def __x_callback(self, option, opt, value, parser): 226 self.__mount = True 227 setattr(parser.values, option.dest, True) 228 if not os.path.exists(SEUNSHARE): 229 raise ValueError(_(""" 230 %s is required for the action you want to perform. 231 """) % SEUNSHARE) 232 233 if not os.path.exists(SANDBOXSH): 234 raise ValueError(_(""" 235 %s is required for the action you want to perform. 236 """) % SANDBOXSH) 237 238 def __validdir(self, option, opt, value, parser): 239 if not os.path.isdir(value): 240 raise IOError("Directory " + value + " not found") 241 setattr(parser.values, option.dest, value) 242 self.__mount = True 243 244 def __include(self, option, opt, value, parser): 245 rp = os.path.realpath(os.path.expanduser(value)) 246 if not os.path.exists(rp): 247 raise IOError(value + " not found") 248 249 if rp not in self.__init_files: 250 self.__init_files.append(rp) 251 252 def __includefile(self, option, opt, value, parser): 253 fd = open(value, "r") 254 for i in fd.readlines(): 255 try: 256 self.__include(option, opt, i[:-1], parser) 257 except IOError as e: 258 sys.stderr.write(str(e)) 259 except TypeError as e: 260 sys.stderr.write(str(e)) 261 fd.close() 262 263 def __copyfiles(self): 264 files = self.__init_files + self.__paths 265 homedir = pwd.getpwuid(os.getuid()).pw_dir 266 for f in files: 267 copyfile(f, homedir, self.__homedir) 268 copyfile(f, "/tmp", self.__tmpdir) 269 copyfile(f, "/var/tmp", self.__tmpdir) 270 271 def __setup_sandboxrc(self, wm="/usr/bin/openbox"): 272 execfile = self.__homedir + "/.sandboxrc" 273 fd = open(execfile, "w+") 274 if self.__options.session: 275 fd.write("""#!/bin/sh 276 #TITLE: /etc/gdm/Xsession 277 /etc/gdm/Xsession 278 """) 279 else: 280 command = self.__paths[0] + " " 281 for p in self.__paths[1:]: 282 command += "'%s' " % p 283 fd.write("""#! /bin/sh 284 #TITLE: %s 285 # /usr/bin/test -r ~/.xmodmap && /usr/bin/xmodmap ~/.xmodmap 286 %s & 287 WM_PID=$! 288 if which dbus-run-session >/dev/null 2>&1; then 289 dbus-run-session -- %s 290 else 291 dbus-launch --exit-with-session %s 292 fi 293 kill -TERM $WM_PID 2> /dev/null 294 """ % (command, wm, command, command)) 295 fd.close() 296 os.chmod(execfile, 0o700) 297 298 def usage(self, message=""): 299 error_exit("%s\n%s" % (self.__parser.usage, message)) 300 301 def __parse_options(self): 302 from optparse import OptionParser 303 types = "" 304 try: 305 types = _(""" 306 Policy defines the following types for use with the -t: 307 \t%s 308 """) % "\n\t".join(next(sepolicy.info(sepolicy.ATTRIBUTE, "sandbox_type"))['types']) 309 except StopIteration: 310 pass 311 312 usage = _(""" 313 sandbox [-h] [-l level ] [-[X|M] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [ -w windowsize ] [[-i file ] ...] [ -t type ] command 314 315 sandbox [-h] [-l level ] [-[X|M] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [ -w windowsize ] [[-i file ] ...] [ -t type ] -S 316 %s 317 """) % types 318 319 parser = OptionParser(usage=usage) 320 parser.disable_interspersed_args() 321 parser.add_option("-i", "--include", 322 action="callback", callback=self.__include, 323 type="string", 324 help=_("include file in sandbox")) 325 parser.add_option("-I", "--includefile", action="callback", callback=self.__includefile, 326 type="string", 327 help=_("read list of files to include in sandbox from INCLUDEFILE")) 328 parser.add_option("-t", "--type", dest="setype", action="store", default=None, 329 help=_("run sandbox with SELinux type")) 330 parser.add_option("-M", "--mount", 331 action="callback", callback=self.__mount_callback, 332 help=_("mount new home and/or tmp directory")) 333 334 parser.add_option("-d", "--dpi", 335 dest="dpi", action="store", 336 help=_("dots per inch for X display")) 337 338 parser.add_option("-S", "--session", action="store_true", dest="session", 339 default=False, help=_("run complete desktop session within sandbox")) 340 341 parser.add_option("-s", "--shred", action="store_true", dest="shred", 342 default=False, help=_("Shred content before tempory directories are removed")) 343 344 parser.add_option("-X", dest="X_ind", 345 action="callback", callback=self.__x_callback, 346 default=False, help=_("run X application within a sandbox")) 347 348 parser.add_option("-H", "--homedir", 349 action="callback", callback=self.__validdir, 350 type="string", 351 dest="homedir", 352 help=_("alternate home directory to use for mounting")) 353 354 parser.add_option("-T", "--tmpdir", dest="tmpdir", 355 type="string", 356 action="callback", callback=self.__validdir, 357 help=_("alternate /tmp directory to use for mounting")) 358 359 parser.add_option("-w", "--windowsize", dest="windowsize", 360 type="string", default=DEFAULT_WINDOWSIZE, 361 help="size of the sandbox window") 362 363 parser.add_option("-W", "--windowmanager", dest="wm", 364 type="string", 365 default="/usr/bin/openbox", 366 help=_("alternate window manager")) 367 368 parser.add_option("-l", "--level", dest="level", 369 help=_("MCS/MLS level for the sandbox")) 370 371 parser.add_option("-C", "--capabilities", 372 action="store_true", dest="usecaps", default=False, 373 help="Allow apps requiring capabilities to run within the sandbox.") 374 375 self.__parser = parser 376 377 self.__options, cmds = parser.parse_args() 378 379 if self.__options.X_ind: 380 self.setype = DEFAULT_X_TYPE 381 else: 382 try: 383 next(sepolicy.info(sepolicy.TYPE, "sandbox_t")) 384 except StopIteration: 385 raise ValueError(_("Sandbox Policy is not currently installed.\nYou need to install the selinux-policy-sandbox package in order to run this command")) 386 387 if self.__options.setype: 388 self.setype = self.__options.setype 389 390 if self.__mount: 391 self.__validate_mount() 392 393 if self.__options.session: 394 if not self.__options.setype: 395 self.setype = selinux.getcon()[1].split(":")[2] 396 if not self.__options.homedir or not self.__options.tmpdir: 397 self.usage(_("You must specify a Homedir and tempdir when setting up a session sandbox")) 398 if len(cmds) > 0: 399 self.usage(_("Commands are not allowed in a session sandbox")) 400 self.__options.X_ind = True 401 self.__homedir = self.__options.homedir 402 self.__tmpdir = self.__options.tmpdir 403 else: 404 if self.__options.level: 405 self.__homedir = self.__options.homedir 406 self.__tmpdir = self.__options.tmpdir 407 408 if len(cmds) == 0: 409 self.usage(_("Command required")) 410 cmds[0] = fullpath(cmds[0]) 411 if not os.access(cmds[0], os.X_OK): 412 self.usage(_("%s is not an executable") % cmds[0]) 413 414 self.__cmds = cmds 415 416 for f in cmds: 417 rp = os.path.realpath(f) 418 if os.path.exists(rp): 419 self.__paths.append(rp) 420 else: 421 self.__paths.append(f) 422 423 def __gen_context(self): 424 if self.__options.level: 425 level = self.__options.level 426 else: 427 level = gen_mcs() 428 429 con = selinux.getcon()[1].split(":") 430 self.__execcon = "%s:%s:%s:%s" % (con[0], con[1], self.setype, level) 431 self.__filecon = "%s:object_r:sandbox_file_t:%s" % (con[0], level) 432 433 def __setup_dir(self): 434 selinux.setfscreatecon(self.__filecon) 435 if self.__options.homedir: 436 self.__homedir = self.__options.homedir 437 else: 438 self.__homedir = mkdtemp(dir="/tmp", prefix=".sandbox_home_") 439 440 if self.__options.tmpdir: 441 self.__tmpdir = self.__options.tmpdir 442 else: 443 self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox_tmp_") 444 self.__copyfiles() 445 selinux.chcon(self.__homedir, self.__filecon, recursive=True) 446 selinux.chcon(self.__tmpdir, self.__filecon, recursive=True) 447 selinux.setfscreatecon(None) 448 449 def __execute(self): 450 try: 451 cmds = [SEUNSHARE, "-Z", self.__execcon] 452 if self.__options.usecaps: 453 cmds.append('-C') 454 if self.__mount: 455 cmds += ["-t", self.__tmpdir, "-h", self.__homedir] 456 457 if self.__options.X_ind: 458 if self.__options.dpi: 459 dpi = self.__options.dpi 460 else: 461 import gi 462 gi.require_version('Gtk', '3.0') 463 from gi.repository import Gtk 464 dpi = str(Gtk.Settings.get_default().props.gtk_xft_dpi / 1024) 465 466 xmodmapfile = self.__homedir + "/.xmodmap" 467 xd = open(xmodmapfile, "w") 468 subprocess.Popen(["/usr/bin/xmodmap", "-pke"], stdout=xd).wait() 469 xd.close() 470 471 self.__setup_sandboxrc(self.__options.wm) 472 473 cmds += ["--", SANDBOXSH, self.__options.windowsize, dpi] 474 else: 475 cmds += ["--"] + self.__paths 476 return subprocess.Popen(cmds).wait() 477 478 pid = os.fork() 479 if pid == 0: 480 rc = os.setsid() 481 if rc: 482 return rc 483 selinux.setexeccon(self.__execcon) 484 os.execv(self.__cmds[0], self.__cmds) 485 rc = os.waitpid(pid, 0) 486 return os.WEXITSTATUS(rc[1]) 487 488 finally: 489 for i in self.__paths: 490 if i not in SAVE_FILES: 491 continue 492 (dest, mtime) = SAVE_FILES[i] 493 if os.path.getmtime(dest) > mtime: 494 savefile(dest, i, self.__options.X_ind) 495 496 if self.__homedir and not self.__options.homedir: 497 if self.__options.shred: 498 self.shred(self.__homedir) 499 shutil.rmtree(self.__homedir) 500 if self.__tmpdir and not self.__options.tmpdir: 501 if self.__options.shred: 502 self.shred(self.__homedir) 503 shutil.rmtree(self.__tmpdir) 504 505 def shred(self, path): 506 for root, dirs, files in os.walk(path): 507 for f in files: 508 dest = root + "/" + f 509 subprocess.Popen(["/usr/bin/shred", dest]).wait() 510 511 def main(self): 512 try: 513 self.__parse_options() 514 self.__gen_context() 515 if self.__mount: 516 self.__setup_dir() 517 return self.__execute() 518 except KeyboardInterrupt: 519 sys.exit(0) 520 521 522 if __name__ == '__main__': 523 setup_sighandlers() 524 if selinux.is_selinux_enabled() != 1: 525 error_exit("Requires an SELinux enabled system") 526 527 try: 528 sandbox = Sandbox() 529 rc = sandbox.main() 530 except OSError as error: 531 error_exit(error) 532 except ValueError as error: 533 error_exit(error.args[0]) 534 except KeyError as error: 535 error_exit(_("Invalid value %s") % error.args[0]) 536 except IOError as error: 537 error_exit(error) 538 except KeyboardInterrupt: 539 rc = 0 540 541 sys.exit(rc) 542