Home | History | Annotate | Download | only in sandbox
      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