From 60a7d2d5447823cab952d84559bc94371bd3176e Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Wed, 18 Oct 2017 16:49:45 +0100 Subject: [PATCH] fishdescriptor: new approach Signed-off-by: Ian Jackson --- fishdescriptor/py/fishdescriptor/fish.py | 146 ++++++++++++++++---- fishdescriptor/py/fishdescriptor/indonor.py | 124 ++++++++++++----- 2 files changed, 210 insertions(+), 60 deletions(-) diff --git a/fishdescriptor/py/fishdescriptor/fish.py b/fishdescriptor/py/fishdescriptor/fish.py index 101a461..95694ff 100644 --- a/fishdescriptor/py/fishdescriptor/fish.py +++ b/fishdescriptor/py/fishdescriptor/fish.py @@ -2,38 +2,132 @@ import socket import subprocess +import os +import pwd + +def _shuffle_fd3(): + os.dup(1,3) + os.dup(2,1) class Donor(): - def __init__(s): - pass + def __init__(d, pid): + d._pid = pid + d._sp = subprocess.Popen( + preexec_fn = _suffle_fd3, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = False, + args = ['gdb', '-p', pid, '-batch', '-ex' + 'python import fishdescriptor.indonor as id;'+ + ' id.DonorImplementation().eval_loop()' + ] + ) - def _ancilmsg(fds): - ''' + def _eval_integer(d, expr): + l = d._sp.stdin.readline() + if l != '!\n': raise RuntimeError("indonor said %s" % repr(l)) + d._sp.stdout.write(expr + '\n') + d._sp.stdout.flush() + l = d._sp.stdin.readline().rstrip('\n') + return int(l) + + def _eval_success(d, expr): + r = d._eval_integer(expr) + if r != 1: raise RuntimeError("eval of %s gave %d" % (expr, r)) + + def _geteuid(d): + return d._eval_integer('di.geteuid()') + + def _ancilmsg(d, fds): + perl_script = ''' + use strict; use Socket; use Socket::MsgHdr; my $fds = pack "i*", @ARGV; my $m = Socket::MsgHdr::pack_cmsghdr SOL_SOCKET, SCM_RIGHTS, $fds; - print join ", ", unpack "C*", $m + print join ", ", unpack "C*", $m; ''' + ap = subprocess.Popen( + stdin = subprocess.DEVNULL, + stdout = subprocess.PIPE, + args = ['perl','-we',perl_script] + fds + ) + (output, dummy) = ap.communicate() + return output + + def donate(d, path, fds): + ancil = d._ancilmsg(fds) + d._eval_success('di.donate(%s, %s)' + % (repr(path), ancil)) + return len(ancil.split(',')) + + def mkdir(d, path): + d._eval_integer('di.mkdir(%s)' + % (repr(path))) + + def _exists(d, path): + try: + os.stat(path) + return True + except OSError as oe: + if oe.errno != os.errno.ENOENT: raise oe + return False + + def _sock_dir(d, target_euid): + run_dir = '/run/user/%d' % target_euid + if d._exists(run_dir): + return run_dir + 'fishdescriptor' + + try: + pw = pwd.getpwuid(target_euid) + return pw.pw_dir + '.fishdescriptor' + except KeyError: + pass + + raise RuntimeError( + 'cannot find good socket path - no /run/user/UID nor pw entry for target process euid %d' + % target_euid + ) + + def fish(d, fds): + # -> list of fds in our process + + euid = d._geteuid() + sockdir = d._sock_dir(euid) + d.mkdir(sockdir) + + sockname = '%s/%s,%d' % (sockdir, os.uname().nodename, d._pid) + + target_root = '/proc/%d/root/' % d._pid + if not d._exists(target_root): + target_root = '' + + our_sockname = target_root + sockname + + s = None + s2 = None + + try: + try: os.remove(our_sockname) + except FileNotFoundError: pass + + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.bind(our_sockname) + s.listen(1) + + ancil_len = d.donate(our_sockname, fds) + s2 = s.accept() + (msg, ancil, flags, sender) = s2.recvmsg(1, ancil_len) + + got_fds = [ ] + + for clvl, ctype, cdata in ancil: + if clvl == socket.SOL_SOCKET and ctype == socket.SCM_RIGHTS: + got_fds += cdata # need to trim any surplus, and unpack + + finally: + if s is not None: s.close() + if s2 is not None: s2.close() -def _geteuid(pid): - def _shuffle_fd3(): - dup(1,3) - dup(2,1) - sp = subprocess.Popen(preexec_fn = _suffle_fd3, - stdin = subprocess.DEVNULL, stdout = subprocess.PIPE, - close_fds = False, - args = ['gdb', '-p', pid, '-batch', '-ex' - 'python import os; os.fdopen(30,"w").write("%d\n" % ' - +'gdb.parse_and_eval("(uid_t)geteuid()"))']) - (output, dummy) = sp.communicate() - -2009 - -def fish(pid, fds): - # -> list of fds in our process - sockname = '/run/user/' + - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - -def deliver(pid, fds, path): - gdb -batch -p %d -ex ' + try: os.remove(our_sockname) + except FileNotFoundError: pass diff --git a/fishdescriptor/py/fishdescriptor/indonor.py b/fishdescriptor/py/fishdescriptor/indonor.py index 74c34a1..046e6f5 100644 --- a/fishdescriptor/py/fishdescriptor/indonor.py +++ b/fishdescriptor/py/fishdescriptor/indonor.py @@ -8,7 +8,7 @@ import os def _string_escape_for_c(s): out = '' for c in bytearray(s): # gets us bytes in py2 and py3 - if c == ord('\\') or c == ord('=') or c < 32 or c > 126: + if c == ord('\\') or c == ord('"') or c < 32 or c > 126: out += '\\x%02x' % c else: out += chr(c) @@ -61,101 +61,157 @@ class DonorStructLayout(): return '((%s)%s)' % (l._typename, _lit_aggregate_uncasted(build)) class DonorImplementation(): - def __init__(d): - d._structs = { } + def __init__(di): + di._structs = { } + di._saved_errno = None + di._result_stream = os.fdopen(3, 'w') # assembling structs # sigh, we have to record the order of the arguments! - def d._find_fields(typename): + def di._find_fields(typename): try: - fields = d._structs[typename] + fields = di._structs[typename] except AttributeError: fields = DonorStructLayout(typename) - d._structs[typename] = fields + di._structs[typename] = fields return fields - def d._make(typename, values): - fields = d._find_fields(typename) - return fieldd.substitute(values) + def di._make(typename, values): + fields = di._find_fields(typename) + return fields.substitute(values) # calling functions (need to cast the function name to the right # type in case maybe gdb doesn't know the type) - def _func(d, functype, funcname, realargs): + def _func(di, functype, funcname, realargs): expr = '((%s) %s) %s' % (functype, funcname, realargs) return gdb.parse_and_eval(expr) - def _must_func(d, functype, funcname, realargs): - retval = d._func(functype, funcname, realargs) + def _must_func(di, functype, funcname, realargs): + retval = di._func(functype, funcname, realargs) if retval < 0: errnoval = gdb.parse_and_eval('errno') raise RuntimeError("%s gave errno=%d `%s'" % - (funcname, errnoval, od.strerror(errnoval))) + (funcname, errnoval, os.strerror(errnoval))) # wrappers for the syscalls that do what we want - def _sendmsg(d, carrier, control_msg): + def _sendmsg(di, carrier, control_msg): iov_base = _lit_array('int', map(str,fds)) - iov = d._make('struct iovec', { + iov = di._make('struct iovec', { 'iov_base': iov_base, 'iov_len' : len(fds), }) - msg = d._make('struct msghdr', { + msg = di._make('struct msghdr', { 'msg_iov' : _lit_addressof(iov), 'msg_iovlen' : 1, 'msg_control' : _lit_array('char', control_msg), 'msg_controllen': len(control_msg), }) - d._must_func( + di._must_func( 'ssize_t (*)(int, const struct msghdr*, int flags)', 'sendmsg', '(%s, %s, 0)' % (carrier, _lit_addressof(msg)) ) - def _socket(d): - return d._must_func( + def _socket(di): + return di._must_func( 'int (*)(int, int, int)', 'socket', '(%d, %d, 0)' % (socket.AF_UNIX, socket.SOCK_STREAM) ) - def _connect(d, fd, path): - addr = d._make('struct sockaddr_un', { + def _connect(di, fd, path): + addr = di._make('struct sockaddr_un', { 'sun_family' : _lit_integer(socket.AF_UNIX), 'sun_path' : _lit_string_uncasted(path), }) - d._must_func( + di._must_func( 'int (*)(int, const struct sockaddr*, socklen_t)', 'connect', '(%d, (const struct sockaddr*)%s, sizeof(struct sockaddr_un))' % (fd, _lit_addressof(addr) ) - def _close(d, fd): - d._must_func('int (*)(int)', 'close', '(%d)' % fd) + def _close(di, fd): + di._must_func('int (*)(int)', 'close', '(%d)' % fd) + + def _mkdir(di, path, mode): + r = di._func( + 'int (*)(const char*, mode_t)', + 'mkdir', + '("%s", %d)' % (_string_escape_for_c(path), mode) + ) + if r < 0: + errnoval = gdb.parse_and_eval('errno') + if errnoval != os.errno.EEXIST: + raise RuntimeError("mkdir %s failed: `%s'" % + (repr(path), os.strerror(errnoval))) + return 0 + return 1 + + def _errno_save(di): + di._saved_errno = gdb.parse_and_eval('errno') + + def _errno_restore(di): + to_restore = di._saved_errno + di._saved_errno = None + if to_restore is not None: + gdb.parse_and_eval('errno = %d' % to_restore)) # main entrypoints - def donate(d, path, control_msg): + def result(di, output): + di._result_stram.write(output) + di._result_stram.flush() + + def donate(di, path, control_msg): # control_msg is an array of integers being the ancillary data # array ("control") for sendmsg, and hence specifies which fds # to pass carrier = None - errnoval = None try: - errnoval = gdb.parse_and_eval('errno') - carrier = d._socket() - d._connect(carrier, path) - d._sendmsg(carrier, control_msg) - d._close(carrier) + di._errno_save() + carrier = di._socket() + di._connect(carrier, path) + di._sendmsg(carrier, control_msg) + di._close(carrier) carrier = None finally: if carrier is not None: - try: d._close(carrier) + try: di._close(carrier) except Exception: pass - if errnoval is not None: - gdb.parse_and_eval('errno = %d' % errnoval) + di._errno_restore() + + di._result('1\n') + + def geteuid(di): + try: + di._errno_save() + val = di._must_func('uid_t (*)(void)', 'geteuid', '()') + finally: + di._errno_restore() + + di._result('%d\n' % val) + + def mkdir(di, path): + try: + di._errno_save() + val = di._mkdir(path, '0700') + finally: + di._errno_restore() + + di._result('%d\n' % val) + + def _protocol_read(di): + return sys.stdin.readline().rstrip('\n') + + def eval_loop(di): + while True: + di._result('!\n') + cmd = di._protocol_read() + eval(cmd) -- 2.30.2