chiark / gitweb /
Merge remote-tracking branch 'mariner/fishdescriptor'
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 20 Apr 2018 15:55:14 +0000 (16:55 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 20 Apr 2018 15:55:14 +0000 (16:55 +0100)
debian/changelog
debian/control
debian/copyright
debian/rules
fishdescriptor/.gitignore [new file with mode: 0644]
fishdescriptor/Makefile [new file with mode: 0644]
fishdescriptor/fishdescriptor [new file with mode: 0755]
fishdescriptor/py/fishdescriptor/__init__.py [new file with mode: 0644]
fishdescriptor/py/fishdescriptor/fish.py [new file with mode: 0644]
fishdescriptor/py/fishdescriptor/indonor.py [new file with mode: 0644]
settings.make

index 177dd70f6ba7b054ecd9761d992c2e5692f82497..1bf6008ac6dff10d390e424e2d44a93620497caf 100644 (file)
@@ -5,6 +5,13 @@ chiark-utils (5.0.3~iwj) unstable; urgency=medium
 
  --
 
+chiark-utils (5.0.3~citrix3) unstable; urgency=medium
+
+  fishdescriptor:
+  * New utility.
+
+ -- Ian Jackson <ian.jackson@citrix.com>  Fri, 20 Apr 2018 16:46:48 +0100
+
 chiark-utils (5.0.3~citrix2) unstable; urgency=medium
 
   with-lock-ex:
index 3e095540e7df876c1838a52ee661b9a7affc37fa..11e15594c29e4ad7b024df921291cb1c81aaba66 100644 (file)
@@ -26,13 +26,17 @@ Priority: extra
 Conflicts: chiark-named-conf, sync-accounts
 Replaces: chiark-named-conf, sync-accounts
 Depends: ${misc:Depends}
-Suggests: tcl8.4
+Suggests: tcl8.4, python3, gdb
 Architecture: all
 Description: chiark system administration scripts
  This package contains a number of small administration scripts used
  by chiark.greenend.org.uk and other systems belonging to the Sinister
  Greenend Organisation.  Featuring:
  .
+ fishdescriptor: a tool for extracting a file descriptor from
+ another (non-cooperating) process and giving it to you (or
+ for examining it).  Requires gdb and python3.
+ .
  chiark-named-conf: a tool for managing nameserver configurations
  and checking for suspected DNS problems.  Its main functions are to
  check that delegations are appropriate and working, that secondary
index 1a719585c843660d5510625bff7a714b4b44118f..5f44a809562a6aed13f2af9e5d375f5f8b963f87 100644 (file)
@@ -88,6 +88,9 @@ with-lock-ex
  Copyright 2017      Ian Jackson in all jurisdictions
  Copyright 2017      Genome Research Ltd
 
+fishdescriptor
+ Copyright 2018 Citrix
+
 The chiark utilities are all free software; you can redistribute them
 and/or modify them under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 3 of the
index 32f312912b87f42b97d5d3508ceb3bf1d451a772..cc926b8e147e35939c3d63fd0ea32fb177448997 100755 (executable)
@@ -3,7 +3,7 @@
 SHELL=/bin/bash
 
 subdirs_build_arch=    cprogs
-subdirs_nobuild=backup sync-accounts scripts
+subdirs_nobuild=backup sync-accounts scripts fishdescriptor
 package=       chiark-utils
 packages_indep=        chiark-backup chiark-scripts
 packages_arch= chiark-rwbuffer chiark-really chiark-utils-bin
@@ -52,6 +52,7 @@ binary-prep:
        #
        mv $t/cprogs $t/chiark-utils-bin
        #
+       cp -a debian/tmp/fishdescriptor/* debian/tmp/scripts/.
        cp -a debian/tmp/sync-accounts/* debian/tmp/scripts/.
        rm -r debian/tmp/sync-accounts
        mv debian/tmp/scripts debian/tmp/chiark-scripts
diff --git a/fishdescriptor/.gitignore b/fishdescriptor/.gitignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/fishdescriptor/Makefile b/fishdescriptor/Makefile
new file mode 100644 (file)
index 0000000..9cf2ba1
--- /dev/null
@@ -0,0 +1,53 @@
+# Makefile
+
+# This file is part of chiark-utils, a collection of useful programs
+# used on chiark.greenend.org.uk.
+#
+# This file is:
+#  Copyright (C) 2017 Ian Jackson <ian.jackson@eu.citrix.com>
+#  Copyright (C) 2001 Ian Jackson <ijackson@chiark.greenend.org.uk>
+#
+# This is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, consult the Free Software Foundation's
+# website at www.fsf.org, or the GNU Project website at www.gnu.org.
+
+include ../settings.make
+
+SCRIPTS=       fishdescriptor
+MODULES_23=    __init__ indonor
+MODULES_3=     fish
+
+py=py/fishdescriptor
+d2=$(python2dir)/fishdescriptor
+d3=$(python3dir)/fishdescriptor
+
+all:
+
+install:
+               $(INSTALL_DIRECTORY) $(bindir) $(d2) $(d3)
+               set -e; for f in $(SCRIPTS); do \
+                       $(INSTALL_SCRIPT) $$f $(bindir)/$$f; done
+               set -e; for f in $(MODULES_3); do \
+                       $(INSTALL_SHARE) $(py)/$$f.py $(d3)/.; done
+               set -e; for f in $(MODULES_23); do \
+                       $(INSTALL_SHARE) $(py)/$$f.py $(d3)/.; \
+                       ln $(d3)/$$f.py $(d2)/.; done
+
+install-docs:
+
+install-examples:
+
+clean:
+               rm -f *~ ./#*#
+               find -name \*.pyc -print0 | xargs -0r rm --
+
+distclean realclean:   clean
diff --git a/fishdescriptor/fishdescriptor b/fishdescriptor/fishdescriptor
new file mode 100755 (executable)
index 0000000..8abdfff
--- /dev/null
@@ -0,0 +1,198 @@
+#!/usr/bin/python3
+
+# This file is part of chiark-utils, a collection of useful programs
+# used on chiark.greenend.org.uk.
+#
+# This file is:
+#  Copyright 2018 Citrix Systems Ltd
+#
+# This is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, consult the Free Software Foundation's
+# website at www.fsf.org, or the GNU Project website at www.gnu.org.
+
+import sys
+import fishdescriptor.fish
+import optparse
+import re
+import subprocess
+import socket
+import os
+
+donor = None
+
+usage = '''fishdescriptor [-p|--pid] <pid> <action>... [-p|--pid <pid> <action>...]
+
+<action>s
+  [<here-fd>=]<there-fd>
+          fish the openfile referenced by descriptor <there-fd> in
+          (the most recent) <pid> and keep a descriptor onto it;
+          and, optionally, give it the number <here-fd> for exec
+  exec <program> [<arg>...]
+          execute a process with each specified <here>
+          as an actual fd
+  sockinfo
+          calls getsockname/getpeername on the most recent
+          <there-fd>
+
+  -p|-pid <pid>
+          now attach to <pid>, detaching from previous pid
+'''
+
+pending = []
+# list of (nominal, there) where nominal might be None
+
+fdmap = { }
+# fdmap[nominal] = (actual, Donor, there)
+
+def implement_pending():
+    try: actuals = donor.fish([pend[1] for pend in pending])
+    except fishdescriptor.fish.Error as e:
+        print('fishdescriptor error: %s' % e, file=sys.stderr)
+        sys.exit(127)
+    assert(len(actuals) == len(pending))
+    for (nominal, there), actual in zip(pending, actuals):
+        overwriting_info = fdmap.get(nominal)
+        if overwriting_info is not None: os.close(overwriting_info[0])
+        fdmap[nominal] = (actual, donor, there)
+
+def implement_sockinfo(nominal):
+    (actual, tdonor, there) = fdmap[nominal]
+    # socket.fromfd requires the AF.  But of course we don't know the AF.
+    # There isn't a sane way to get it in Python:
+    #  https://utcc.utoronto.ca/~cks/space/blog/python/SocketFromFdMistake
+    # Rejected options:
+    #  https://github.com/tiran/socketfromfd
+    #   adds a dependency, not portable due to reliance on SO_DOMAIN
+    #  call getsockname using ctypes
+    #   no sane way to discover how to unpack sa_family_t
+    perl_script = '''
+        use strict;
+        use Socket;
+        use POSIX;
+        my $sa = getsockname STDIN;
+        exit 0 if !defined $sa and $!==ENOTSOCK;
+        my $family = sockaddr_family $sa;
+        print $family, "\n" or die $!;
+    '''
+    famp = subprocess.Popen(
+        stdin = actual,
+        stdout = subprocess.PIPE,
+        args = ['perl','-we',perl_script]
+    )
+    (output, dummy) = famp.communicate()
+    family = int(output)
+
+    sock = socket.fromfd(actual, family, 0)
+
+    print("[%s] %d sockinfo" % (tdonor.pid, there), end='')
+    for f in (lambda: socket.AddressFamily(family).name,
+              lambda: repr(sock.getsockname()),
+              lambda: repr(sock.getpeername())):
+        try: info = f()
+        except Exception as e: info = repr(e)
+        print("\t", info, sep='', end='')
+    print("")
+
+    sock.close()
+
+def permute_fds_for_exec():
+    actual2intended = { info[0]: nominal for nominal, info in fdmap.items() }
+    # invariant at the start of each loop iteration:
+    #     for each intended (aka `nominal') we have processed:
+    #         relevant open-file is only held in fd intended
+    #         (unless `nominal' is None in which case it is closed)
+    #     for each intended (aka `nominal') we have NOT processed:
+    #         relevant open-file is only held in actual
+    #         where  actual = fdmap[nominal][0]
+    #         and where  actual2intended[actual] = nominal
+    # we can rely on processing each intended only once,
+    #  since they're hash keys
+    # the post-condition is not really a valid state (fdmap
+    #  is nonsense) but we call this function just before exec
+    for intended, (actual, tdonor, there) in fdmap.items():
+        if intended == actual:
+            continue
+        if intended is not None:
+            inway_intended = actual2intended.get(intended)
+            if inway_intended is not None:
+                inway_moved = os.dup(intended)
+                actual2intended[inway_moved] = inway_intended
+                fdmap[inway_intented][0] = inway_moved
+            os.dup2(actual, intended)
+        os.close(actual)
+
+def implement_exec(argl):
+    if donor is not None: donor.detach()
+    sys.stdout.flush()
+    permute_fds_for_exec()
+    os.execvp(argl[0], argl)
+
+def set_donor(pid):
+    global donor
+    if donor is not None: donor.detach()
+    donor = fishdescriptor.fish.Donor(pid, debug=ov.debug)
+
+def ocb_set_donor(option, opt, value, parser):
+    set_donor(value)
+
+ov = optparse.Values()
+
+def process_args():
+    global ov
+
+    m = None
+    
+    def arg_matches(regexp):
+        nonlocal m
+        m = re.search(regexp, arg)
+        return m
+
+    op = optparse.OptionParser(usage=usage)
+
+    op.disable_interspersed_args()
+    op.add_option('-p','--pid', type='int', action='callback',
+                  callback=ocb_set_donor)
+    op.add_option('-D','--debug', action='store_const',
+                  dest='debug', const=sys.stderr)
+    ov.debug = None
+
+    args = sys.argv[1:]
+    last_nominal = None # None or (nominal,) ie None or (None,) or (int,)
+
+    while True:
+        (ov, args) = op.parse_args(args=args, values=ov)
+        if not len(args): break
+
+        arg = args.pop(0)
+
+        if donor is None:
+            set_donor(int(arg))
+        elif arg_matches(r'^(?:(\d+)=)?(\d+)?$'):
+            (nominal, there) = m.groups()
+            nominal = None if nominal is None else int(nominal)
+            there = int(there)
+            pending.append((nominal,there))
+            last_nominal = (nominal,)
+        elif arg == 'exec':
+            if not len(args):
+                op.error("exec needs command to run")
+            implement_pending()
+            implement_exec(args)
+        elif arg == 'sockinfo':
+            if last_nominal is None:
+                op.error('sockinfo needs a prior fd spec')
+            implement_pending()
+            implement_sockinfo(last_nominal[0])
+        else:
+            op.error("unknown argument/option `%s'" % arg)
+
+process_args()
diff --git a/fishdescriptor/py/fishdescriptor/__init__.py b/fishdescriptor/py/fishdescriptor/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/fishdescriptor/py/fishdescriptor/fish.py b/fishdescriptor/py/fishdescriptor/fish.py
new file mode 100644 (file)
index 0000000..0f180bc
--- /dev/null
@@ -0,0 +1,181 @@
+# fish.py
+
+# This file is part of chiark-utils, a collection of useful programs
+# used on chiark.greenend.org.uk.
+#
+# This file is:
+#  Copyright 2018 Citrix Systems Ltd
+#
+# This is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, consult the Free Software Foundation's
+# website at www.fsf.org, or the GNU Project website at www.gnu.org.
+
+# python 3 only
+
+import socket
+import subprocess
+import os
+import pwd
+import struct
+import tempfile
+import shutil
+import sys
+
+def _shuffle_fd3():
+    os.dup2(1,3)
+    os.dup2(2,1)
+
+class Error(Exception): pass
+
+class Donor():
+    def __init__(d, pid, debug=None):
+        d.pid = pid
+        if debug is None:
+            d._stderr = tempfile.TemporaryFile(mode='w+')
+        else:
+            d._stderr = None
+        d._sp = subprocess.Popen(
+            preexec_fn = _shuffle_fd3,
+            stdin = subprocess.PIPE,
+            stdout = subprocess.PIPE,
+            stderr = d._stderr,
+            close_fds = False,
+            args = ['gdb', '-p', str(pid), '-batch', '-ex',
+                    'python import fishdescriptor.indonor as id;'+
+                    ' id.DonorImplementation().eval_loop()'
+                ]
+        )            
+
+    def _eval_integer(d, expr):
+        try:
+            l = d._sp.stdout.readline()
+            if not len(l): raise Error('gdb process donor python repl quit')
+            if l != b'!\n': raise RuntimeError("indonor said %s" % repr(l))
+            d._sp.stdin.write(expr.encode('utf-8') + b'\n')
+            d._sp.stdin.flush()
+            l = d._sp.stdout.readline().rstrip(b'\n')
+            return int(l)
+        except Exception as e:
+            if d._stderr is not None:
+                d._stderr.seek(0)
+                shutil.copyfileobj(d._stderr, sys.stderr)
+                d._stderr.seek(0)
+                d._stderr.truncate()
+            raise e
+
+    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;
+        '''
+        ap = subprocess.Popen(
+            stdin = subprocess.DEVNULL,
+            stdout = subprocess.PIPE,
+            args = ['perl','-we',perl_script] + [str(x) for x in fds]
+        )
+        (output, dummy) = ap.communicate()
+        return output.decode('utf-8')
+
+    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, dummy) = s.accept()
+            (msg, ancil, flags, sender) = s2.recvmsg(1, ancil_len)
+
+            got_fds = None
+            unpack_fmt = '%di' % len(fds)
+
+            for clvl, ctype, cdata in ancil:
+                if clvl == socket.SOL_SOCKET and ctype == socket.SCM_RIGHTS:
+                    assert(got_fds is None)
+                    got_fds = struct.unpack_from(unpack_fmt, cdata)
+
+        finally:
+            if s is not None: s.close()
+            if s2 is not None: s2.close()
+
+            try: os.remove(our_sockname)
+            except FileNotFoundError: pass
+
+        return list(got_fds)
+
+    def detach(d):
+        d._sp.stdin.close()
diff --git a/fishdescriptor/py/fishdescriptor/indonor.py b/fishdescriptor/py/fishdescriptor/indonor.py
new file mode 100644 (file)
index 0000000..20bc807
--- /dev/null
@@ -0,0 +1,291 @@
+
+# This file is part of chiark-utils, a collection of useful programs
+# used on chiark.greenend.org.uk.
+#
+# This file is:
+#  Copyright 2018 Citrix Systems Ltd
+#
+# This is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, consult the Free Software Foundation's
+# website at www.fsf.org, or the GNU Project website at www.gnu.org.
+
+# class for use inside gdb which is debugging the donor process
+
+from __future__ import print_function
+
+import gdb
+import copy
+import os
+import sys
+import socket
+
+def _string_bytearray(s):
+    # gets us bytes in py2 and py3
+    if not isinstance(s, bytes):
+        s = s.encode('utf-8') # sigh, python 2/3 compat
+    return bytearray(s)
+
+def _string_escape_for_c(s):
+    out = ''
+    for c in _string_bytearray(s):
+        if c == ord('\\') or c == ord('"') or c < 32 or c > 126:
+            out += '\\x%02x' % c
+        else:
+            out += chr(c)
+    return out
+
+# constructing values
+
+def _lit_integer(v):
+    return '%d' % v
+
+def _lit_aggregate_uncasted(val_lit_strs):
+    return '{' + ', '.join(['(%s)' % v for v in val_lit_strs]) + ' }'
+
+def _lit_string_uncasted(s):
+    b = _string_bytearray(s)
+    return _lit_aggregate_uncasted([_lit_integer(x) for x in b] + [ '0' ])
+
+def _lit_array(elemtype, val_lit_strs):
+    return (
+        '((%s[%d])%s)' %
+        (elemtype, len(val_lit_strs), _lit_aggregate_uncasted(val_lit_strs))
+    )
+
+def _lit_addressof(v):
+    return '&(char[])(%s)' % v
+
+def _make_lit(v):
+    if isinstance(v, int):
+        return _lit_integer(v)
+    else:
+        return v # should already be an integer
+
+def parse_eval(expr):
+    sys.stderr.write("##  EVAL %s\n" % repr(expr))
+    x = gdb.parse_and_eval(expr)
+    sys.stderr.write('##  => %s\n' % x)
+    sys.stderr.flush()
+    return x
+
+class DonorStructLayout():
+    def __init__(l, typename):
+        x = gdb.lookup_type(typename)
+        l._typename = typename
+        l._template = [ ]
+        l._posns = { }
+        for f in x.fields():
+            l._posns[f.name] = len(l._template)
+            try: f.type.fields();  blank = '{ }'
+            except TypeError:      blank = '0'
+            except AttributeError: blank = '0'
+            l._template.append(blank)
+        sys.stderr.write('##  STRUCT %s template %s fields %s\n'
+                         % (typename, l._template, l._posns))
+
+    def substitute(l, values):
+        build = copy.deepcopy(l._template)
+        for (k,v) in values.items():
+            build[ l._posns[k] ] = _make_lit(v)
+        return '((%s)%s)' % (l._typename, _lit_aggregate_uncasted(build))
+
+class DonorImplementation():
+    def __init__(di):
+        di._structs = { }
+        di._saved_errno = None
+        di._result_stream = os.fdopen(3, 'w')
+        di._errno_workaround = None
+
+    # assembling structs
+    # sigh, we have to record the order of the arguments!
+    def _find_fields(di, typename):
+        try:
+            fields = di._structs[typename]
+        except KeyError:
+            fields = DonorStructLayout(typename)
+            di._structs[typename] = fields
+        return fields
+
+    def _make(di, typename, values):
+        fields = di._find_fields(typename)
+        return fields.substitute(values)
+
+    # hideous workaround
+
+    def _parse_eval_errno(di, expr_pat):
+        # evaluates  expr_pat % 'errno'
+        if di._errno_workaround is not True:
+            try:
+                x = parse_eval(expr_pat % 'errno')
+                di._errno_workaround = False
+                return x
+            except gdb.error as e:
+                if di._errno_workaround is False:
+                    raise e
+            di._errno_workaround = True
+        # Incomprehensibly, gdb.parse_and_eval('errno') can sometimes
+        # fail with
+        #   gdb.error: Cannot find thread-local variables on this target
+        # even though plain gdb `print errno' works while `print errno = 25'
+        # doesn't.   OMG.  This may be related to:
+        #  https://github.com/cloudburst/libheap/issues/24
+        # although I can't find it in the gdb bug db (which is half-broken
+        # in my browser).  Also the error is very nonspecific :-/.
+        # This seems to happen on jessie, and is fixed in stretch.
+        # Anyway:
+        return parse_eval(expr_pat % '(*((int (*)(void))__errno_location)())')
+
+    # calling functions (need to cast the function name to the right
+    # type in case maybe gdb doesn't know the type)
+
+    def _func(di, functype, funcname, realargs):
+        expr = '((%s) %s) %s' % (functype, funcname, realargs)
+        return parse_eval(expr)
+
+    def _must_func(di, functype, funcname, realargs):
+        retval = di._func(functype, funcname, realargs)
+        if retval < 0:
+            errnoval = di._parse_eval_errno('%s')
+            raise RuntimeError("%s gave errno=%d `%s'" %
+                               (funcname, errnoval, os.strerror(errnoval)))
+        return retval
+
+    # wrappers for the syscalls that do what we want
+
+    def _sendmsg(di, carrier, control_msg):
+        iov_base = _lit_array('char', [1])
+        iov = di._make('struct iovec', {
+            'iov_base': iov_base,
+            'iov_len' : 1,
+        })
+
+        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),
+        })
+
+        di._must_func(
+            'ssize_t (*)(int, const struct msghdr*, int)',
+            'sendmsg',
+            '(%s, %s, 0)' % (carrier, _lit_addressof(msg))
+        )
+
+    def _socket(di):
+        return di._must_func(
+            'int (*)(int, int, int)',
+            'socket',
+            '(%d, %d, 0)' % (socket.AF_UNIX, socket.SOCK_STREAM)
+        )
+
+    def _connect(di, fd, path):
+        addr = di._make('struct sockaddr_un', {
+            'sun_family' : _lit_integer(socket.AF_UNIX),
+            'sun_path'   : _lit_string_uncasted(path),
+        })
+
+        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(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 = di._parse_eval_errno('%s')
+            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 = di._parse_eval_errno('%s')
+
+    def _errno_restore(di):
+        to_restore = di._saved_errno
+        di._saved_errno = None
+        if to_restore is not None:
+            di._parse_eval_errno('%%s = %d' % to_restore)
+
+    def _result(di, output):
+        sys.stderr.write("#> %s" % output)
+        di._result_stream.write(output)
+        di._result_stream.flush()
+
+    # main entrypoints
+
+    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
+        try:
+            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: di._close(carrier)
+                except Exception: pass
+            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, int('0700', 8))
+        finally:
+            di._errno_restore()
+
+        di._result('%d\n' % val)
+
+    def _protocol_read(di):
+        input = sys.stdin.readline()
+        if input == '': return None
+        input = input.rstrip('\n')
+        sys.stderr.write("#< %s\n" % input)
+        return input
+
+    def eval_loop(di):
+        if not gdb.selected_inferior().was_attached:
+            print('gdb inferior not attached', file=sys.stderr)
+            sys.exit(0)
+        while True:
+            di._result('!\n')
+            cmd = di._protocol_read()
+            if cmd is None: break
+            eval(cmd)
index ff917b18392e26f16f2a26624e691f6461588ad8..495b2b3f855b47d716d59929186d61ee5079bdf4 100644 (file)
@@ -44,6 +44,8 @@ sbindir=$(prefix)/sbin
 sharedir=$(prefix)/share/$(us)
 perl5dir=$(prefix)/share/perl5
 txtdocdir=$(prefix)/share/doc/$(us)
+python2dir=$(prefix)/lib/python2.7/dist-packages
+python3dir=$(prefix)/lib/python3/dist-packages
 exampledir=$(txtdocdir)/examples
 vardir=$(varlib)/$(us)
 mandir=${prefix}/man