Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/rainbow/inject.py
diff options
context:
space:
mode:
Diffstat (limited to 'rainbow/inject.py')
-rw-r--r--rainbow/inject.py317
1 files changed, 317 insertions, 0 deletions
diff --git a/rainbow/inject.py b/rainbow/inject.py
new file mode 100644
index 0000000..47d0cf9
--- /dev/null
+++ b/rainbow/inject.py
@@ -0,0 +1,317 @@
+import os
+from os import R_OK, W_OK, X_OK, fork, symlink, unlink, O_CREAT, O_EXCL, chown, chmod
+from os import setgroups, setgid, setuid, chdir, umask, execvpe, waitpid, WEXITSTATUS
+from os import getpid, getuid, _exit, rename, readlink
+from os.path import join, basename, realpath, lexists, exists, dirname
+from subprocess import check_call, Popen, PIPE
+from stat import S_IFDIR
+from tempfile import mkdtemp, mkstemp
+from grp import getgrnam, getgrgid
+from pwd import getpwuid
+from glob import glob
+import resource
+
+from rainbow.util import Checker, mount, make_dirs, get_fds, read_envdir
+from rainbow.util import unshare, CLONE_NEWNET
+
+def reserve_elt(pool_dir, elt, max_elt, incr, elt_name):
+ fd = None
+ while elt < max_elt:
+ try:
+ fd = os.open(join(pool_dir, str(elt)), O_EXCL | O_CREAT, 0600)
+ os.close(fd)
+ break
+ except OSError:
+ elt += incr
+ if fd is None:
+ raise RuntimeError("No " + elt_name + " available.")
+ return elt
+
+def reserve_tag(log, spool, tag, tag_map, tag_type, tag_type_plural, min, max, step):
+ # Pick an element for yourself.
+ pool_dir = join(spool, tag_type+'_pool')
+ elt = reserve_elt(pool_dir, min, max, step, tag_type_plural)
+ log(1, 'reserved %s (%d) for tag %s', tag_type, elt, tag)
+
+ # Then try to atomically symlink the elt you picked to 'tag' in 'tag_map' dir.
+ # If you succeed, then you have the right elt.
+ # If you fail, someone else has the right elt so release yours and use theirs.
+ tag_path = join(spool, tag_map, tag)
+ elt_path = join(pool_dir, str(elt))
+ try:
+ symlink(str(elt), tag_path)
+ except OSError:
+ unlink(elt_path)
+ elt = int(basename(realpath(tag_path)))
+ return elt
+
+def add_uid_to_group(log, spool, owner_uid, uid, gid):
+ map_dir = join(spool, 'gid_to_members', str(gid))
+ make_dirs(map_dir, 0, 0, 0755)
+ owner_path = join(spool, 'gid_to_owner', str(gid))
+ if not lexists(owner_path):
+ symlink(str(owner_uid), join(spool, 'gid_to_owner', str(gid)))
+ # Only join groups that we own.
+ assert readlink(owner_path) == str(owner_uid)
+ open(join(map_dir, str(uid)), 'w').close()
+ open(join(map_dir, str(getpwuid(owner_uid).pw_name)), 'w').close()
+ log(1, "added owner (%d) and uid (%d) to gid (%d)", owner_uid, uid, gid)
+
+def reserve_uid(log, spool, owner_uid):
+ gid = reserve_elt(join(spool, 'gid_pool'), 10000, 65534, 1, 'gids')
+ uid = reserve_elt(join(spool, 'uid_pool'), 10000, 65534, 1, 'uids')
+ symlink(str(gid), join(spool, 'uid_to_gid', str(uid)))
+ add_uid_to_group(log, spool, owner_uid, uid, gid)
+ return (uid, gid)
+
+def reserve_group(log, spool, owner_uid, uid, group):
+ gid = reserve_tag(log, spool, group, 'bundle_id_to_gid', 'gid', 'gids', 10000, 65534, 1)
+ add_uid_to_group(log, spool, owner_uid, uid, gid)
+ return gid
+
+def grab_home(_, spool, uid, __, owner_gid):
+ home = join(spool, 'uid_to_home_dir', str(uid))
+ make_dirs(home, uid, owner_gid, 0770)
+ chown(home, uid, owner_gid)
+ # Per discussion with Bert Freudenberg, set the setgid bit on $HOME
+ # (i.e. $HOME) so that Sugar can better write inside it. <MS>
+ chmod(home, 02770)
+ return home
+
+def configure_home(_, spool, home, owner_uid, __, ___, gid, data_group_to_gid):
+ for group, gid in data_group_to_gid:
+ path = join(spool, 'gid_to_data_dir', str(gid))
+ make_dirs(path, owner_uid, gid, 0770)
+ chown(path, owner_uid, gid)
+ chmod(path, 02770)
+ if not lexists(join(home, group)):
+ symlink(path, join(home, group))
+
+def mount_fsen(log, _):
+ log(1, 'Mounting tmpfsen on /var/tmp and... drat; /tmp kills the X socket. :( <MS>')
+ #mount('tmpfs', '/tmp', 'tmpfs', 0, '')
+ mount('tmpfs', '/var/tmp', 'tmpfs', 0, '')
+
+def run_assistant(log, assistant, env, owner_uid, owner_gid, uid, groups, safe_fds):
+ envdir = None
+ try:
+ envdir = mkdtemp()
+ chown(envdir, owner_uid, owner_gid)
+ pid = fork()
+ except:
+ if envdir: check_call(['/bin/rm', '-rf', envdir])
+ raise
+ else:
+ if not pid:
+ log(1, 'Dropping privilege to run assistant.')
+ setgroups(groups)
+ setgid(owner_gid)
+ setuid(owner_uid)
+ log(1, 'Closing fds.')
+ for fd in get_fds():
+ if fd not in safe_fds:
+ try: os.close(fd) # propagate failure from EIO or EBADF.
+ except: pass
+ log(1, 'Running assistant.')
+ assistant_argv = [assistant, '-v', '-v', '-v', '-u', str(uid), '-e', envdir]
+ log(1, '%r %r', assistant_argv, env)
+ execvpe(assistant_argv[0], assistant_argv, env)
+ _exit(55)
+ else:
+ try:
+ pid, status = waitpid(pid, 0)
+ log(1, 'Assistant returned %d.', status)
+ assert not WEXITSTATUS(status)
+ return read_envdir(envdir)
+ finally:
+ log(1, 'pid %d uid %d', getpid(), getuid())
+ if envdir: check_call(['/bin/rm', '-rf', envdir])
+
+def launch(log, _, uid, gid, groups, argv, env, cwd, pset, safe_fds):
+ # Set appropriate group membership(s), depending on requested permissions
+ log(1, 'dropping privilege to (%d, %d, %r)', uid, gid, groups)
+ setgroups(groups)
+ setgid(gid)
+ setuid(uid)
+
+ # Limit various resources
+ # Must be done *after* setting uid/gid
+ # This should come from the permissions.info file, but this is OK
+ # for now.
+ try:
+ def set_limit(lim_name, unix_name):
+ p = pset.permission_params('lim_' + lim_name)
+ if p != None:
+ x = int(float(p[0]))
+ y = int(float(x*1.10))
+ log(1, 'Setting RLIMIT_%s to %d,%d', unix_name, x, y)
+ resource.setrlimit(getattr(resource, 'RLIMIT_'+unix_name), (x,y))
+
+ set_limit('nofile', 'NOFILE')
+ set_limit('fsize', 'FSIZE')
+ set_limit('mem', 'AS')
+ set_limit('nproc', 'NPROC')
+
+ except:
+ pass # Why would we be throwing exceptions when setting rlimits? <MS>
+
+ log(1, 'chdir to %s' % cwd)
+ chdir(cwd)
+
+ log(1, 'umask(0)')
+ umask(0)
+
+ log(1, 'about to execve\nargv: %s\nenv: %s', argv, env)
+ log(1, 'closing all fds but %s', safe_fds)
+ for fd in get_fds():
+ if fd not in safe_fds:
+ try: os.close(fd) # propagate failure from EIO or EBADF.
+ except: pass
+
+ execvpe(argv[0], argv, env)
+
+def check_data_groups(data_groups):
+ # XXX: How do I figure out what MAX_PATH_LEN is in python? <MS>
+ # XXX: How long are GECOS fields permitted to be? <MS>
+ for data_id in data_groups:
+ assert data_id and '\0' not in data_id and len(data_id) < 128
+
+def check_argv(argv):
+ assert argv
+
+def check_cwd(uid, gid, cwd):
+ ck = Checker(cwd, uid, gid)
+ assert ck.positive(R_OK | X_OK, S_IFDIR)
+
+def check_spool(spool, owner_uid, owner_gid):
+ make_dirs(spool, 0, 0, 0755)
+ spool_dirs = ('uid_pool', 'gid_pool', 'uid_to_gid', 'bundle_id_to_gid',
+ 'gid_to_data_dir', 'uid_to_home_dir', 'xephyr_display_pool',
+ 'uid_to_xephyr_cookie', 'uid_to_xephyr_display',
+ 'uid_to_xephyr_auth', 'gid_to_members', 'gid_to_owner')
+ for frag in spool_dirs:
+ make_dirs(join(spool, frag), 0, 0, 0755)
+ ck = Checker(join(spool, frag), owner_uid, owner_gid)
+ assert ck.positive(R_OK | X_OK, S_IFDIR)
+
+def check_owner(_, __):
+ return True
+
+def check_home_dirs(uid, gid, home, data_group_to_gid):
+ for frag, _ in data_group_to_gid:
+ ck = Checker(join(home, frag), uid, gid, [x for _, x in data_group_to_gid])
+ assert ck.positive(R_OK | W_OK | X_OK, S_IFDIR)
+
+def check_home(uid, gid, home):
+ ck = Checker(home, uid, gid)
+ assert ck.positive(R_OK | W_OK | X_OK, S_IFDIR)
+
+def maybe_add_gid(log, owner_uid, gid):
+ # rainbow should only let you drop privilege.
+ owner = getpwuid(owner_uid).pw_name
+ members = getgrgid(gid).gr_mem
+ log(1, "maybe_add_gid owner: %s members: %s result: %s", owner, members, owner in members)
+ return owner in members
+
+def configure_groups(log, owner_uid, groups, gid, data_group_to_gid, recorded_groups, pset):
+ groups.insert(0, gid)
+ groups += recorded_groups
+
+ for _, data_gid in data_group_to_gid:
+ if maybe_add_gid(log, owner_uid, data_gid):
+ groups.insert(0, data_gid)
+
+ for cap in ("audio", "video", "serial"):
+ try:
+ if pset.has_permission(cap):
+ cap_gid = getgrnam(cap).gr_gid
+ if maybe_add_gid(log, owner_uid, cap_gid):
+ groups.insert(0, cap_gid)
+ except Exception, e: log(1, "Skipping permission (%s) because of (%s).", cap, e)
+ return list(set(groups))
+
+def configure_xephyr(_, spool, owner_gid, uid, env, safe_fds):
+ # XXX: MUST CHECK RETURN VALUES on subprocesses!!!!!
+ # XXX: I shouldn't be running these subprocesses as uid 0.
+ # XXX: Must get env, fds right!!!!
+ cookie_path = join(spool, 'uid_to_xephyr_cookie', str(uid))
+ if lexists(cookie_path):
+ cookie = readlink(cookie_path)
+ else:
+ cookie = Popen(["mcookie"], stdout=PIPE).communicate()[0]
+ symlink(cookie, join(spool, 'uid_to_xephyr_cookie', str(uid)))
+
+ display_path = join(spool, 'uid_to_xephyr_display', str(uid))
+ if lexists(display_path):
+ display = int(readlink(display_path))
+ else:
+ display = reserve_elt(join(spool, 'xephyr_display_pool'), 100, 10000, 2, 'displays')
+ symlink(str(display), display_path)
+
+ auth_path = join(spool, 'uid_to_xephyr_auth', str(uid))
+ if not exists(auth_path):
+ fd, name = mkstemp(prefix='tmp', dir=join(spool, 'uid_to_xephyr_auth'))
+ os.close(fd)
+ Popen(["xauth", "-f", name], stdin=PIPE).communicate("add :%d . %s\n" % (display, cookie))
+ rename(name, auth_path)
+ chmod(auth_path, 0640)
+ chown(auth_path, 0, owner_gid)
+
+ # NB: Current versions of Xephyr will exit if the display we specified is
+ # already in use. This permits us to run Xephyr unconditionally even when
+ # we're resuming an old uid. <MS>
+ Popen(["Xephyr", "-screen", "800x600x24", "-auth", auth_path, "-reset", "-terminate", ":%d" % display])
+
+ newenv = {'DISPLAY' : ':%d' % display, 'XAUTHORITY' : auth_path}
+ return newenv
+
+def configure_network(log, pset):
+ log(1, "networking shared with parent: %s", pset.has_permission("network"))
+ if not pset.has_permission("network"):
+ unshare(CLONE_NEWNET)
+
+def check_uid(_, spool, owner_uid, uid):
+ assert 10000 <= uid and uid <= 65534
+ assert getpwuid(owner_uid).pw_name in getgrgid(uid).gr_mem
+
+def inject(log, spool, env, argv, cwd, pset, safe_fds, owner_uid, owner_gid,
+ uid, groups, data_groups, assistant, xephyr):
+ # Note: exceptions are intended to bubble up to the caller and should
+ # terminate execution.
+ check_data_groups(data_groups)
+ check_argv(argv)
+ check_owner(owner_uid, owner_gid)
+
+ check_spool(spool, owner_uid, owner_gid)
+
+ if not uid:
+ uid, gid = reserve_uid(log, spool, owner_uid)
+ home = grab_home(log, spool, uid, gid, owner_gid)
+ else:
+ check_uid(log, spool, owner_uid, uid)
+ pw = getpwuid(uid)
+ gid, home = pw.pw_gid, pw.pw_dir
+ log(1, "resuming uid (%d) for owner (%d) with gid (%d) and home (%s)", uid, owner_uid, gid, home)
+
+ # XXX: Need to verify ownership and membership before joining data groups.
+ recorded_groups = [int(basename(dirname(p))) for p in glob(join(spool, 'gid_to_members', '*', str(uid)))]
+ data_group_to_gid = [(group, reserve_group(log, spool, owner_uid, uid, group)) for group in data_groups]
+ configure_home(log, spool, home, owner_uid, owner_gid, uid, gid, data_group_to_gid)
+
+ if cwd is None:
+ cwd = home
+ check_cwd(uid, gid, cwd)
+ check_home_dirs(uid, gid, home, data_group_to_gid)
+ check_home_dirs(owner_uid, owner_gid, home, data_group_to_gid)
+ check_home(uid, gid, home)
+
+ groups = configure_groups(log, owner_uid, groups, gid, data_group_to_gid, recorded_groups, pset)
+ if xephyr:
+ env.update(configure_xephyr(log, spool, owner_gid, uid, env, safe_fds))
+ if assistant:
+ env.update(run_assistant(log, assistant, env, owner_uid, owner_gid, uid, groups, safe_fds))
+
+ mount_fsen(log, home)
+ configure_network(log, pset)
+
+ launch(log, home, uid, gid, groups, argv, env, cwd, pset, safe_fds)