chiark / gitweb /
718456a28536516e2eaaa472f10e9cd60d87a509
[chiark-utils.git] / fishdescriptor / py / fishdescriptor / fish.py
1 # python 3 only
2
3 import socket
4 import subprocess
5 import os
6 import pwd
7
8 def _shuffle_fd3():
9     os.dup2(1,3)
10     os.dup2(2,1)
11
12 class Donor():
13     def __init__(d, pid):
14         d._pid = pid
15         d._sp = subprocess.Popen(
16             preexec_fn = _shuffle_fd3,
17             stdin = subprocess.PIPE,
18             stdout = subprocess.PIPE,
19             close_fds = False,
20             args = ['gdb', '-p', str(pid), '-batch', '-ex',
21                     'python import fishdescriptor.indonor as id;'+
22                     ' id.DonorImplementation().eval_loop()'
23                 ]
24         )            
25
26     def _eval_integer(d, expr):
27         l = d._sp.stdout.readline()
28         if l != b'!\n': raise RuntimeError("indonor said %s" % repr(l))
29         d._sp.stdin.write(expr.encode('utf-8') + b'\n')
30         d._sp.stdin.flush()
31         l = d._sp.stdout.readline().rstrip(b'\n')
32         return int(l)
33
34     def _eval_success(d, expr):
35         r = d._eval_integer(expr)
36         if r != 1: raise RuntimeError("eval of %s gave %d" % (expr, r))
37
38     def _geteuid(d):
39         return d._eval_integer('di.geteuid()')
40
41     def _ancilmsg(d, fds):
42         perl_script = '''
43             use strict;
44             use Socket;
45             use Socket::MsgHdr;
46             my $fds = pack "i*", @ARGV;
47             my $m = Socket::MsgHdr::pack_cmsghdr SOL_SOCKET, SCM_RIGHTS, $fds;
48             print join ", ", unpack "C*", $m;
49         '''
50         ap = subprocess.Popen(
51             stdin = subprocess.DEVNULL,
52             stdout = subprocess.PIPE,
53             args = ['perl','-we',perl_script] + [str(x) for x in fds]
54         )
55         (output, dummy) = ap.communicate()
56         return output.decode('utf-8')
57
58     def donate(d, path, fds):
59         ancil = d._ancilmsg(fds)
60         d._eval_success('di.donate(%s, [ %s ])'
61                         % (repr(path), ancil))
62         return len(ancil.split(','))
63
64     def mkdir(d, path):
65         d._eval_integer('di.mkdir(%s)'
66                         % (repr(path)))
67
68     def _exists(d, path):
69         try:
70             os.stat(path)
71             return True
72         except OSError as oe:
73             if oe.errno != os.errno.ENOENT: raise oe
74             return False
75
76     def _sock_dir(d, target_euid):
77         run_dir = '/run/user/%d' % target_euid
78         if d._exists(run_dir):
79             return run_dir + '/fishdescriptor'
80
81         try:
82             pw = pwd.getpwuid(target_euid)
83             return pw.pw_dir + '/.fishdescriptor'
84         except KeyError:
85             pass
86
87         raise RuntimeError(
88  'cannot find good socket path - no /run/user/UID nor pw entry for target process euid %d'
89             % target_euid
90         )
91
92     def fish(d, fds):
93         # -> list of fds in our process
94
95         euid = d._geteuid()
96         sockdir = d._sock_dir(euid)
97         d.mkdir(sockdir)
98
99         sockname = '%s/%s,%d' % (sockdir, os.uname().nodename, d._pid)
100
101         target_root = '/proc/%d/root' % d._pid
102         if not d._exists(target_root):
103             target_root = ''
104
105         our_sockname = target_root + sockname
106
107         s = None
108         s2 = None
109
110         try:
111             try: os.remove(our_sockname)
112             except FileNotFoundError: pass
113
114             s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
115             s.bind(our_sockname)
116             s.listen(1)
117
118             ancil_len = d.donate(our_sockname, fds)
119             (s2, dummy) = s.accept()
120             (msg, ancil, flags, sender) = s2.recvmsg(1, ancil_len)
121
122             got_fds = [ ]
123
124             for clvl, ctype, cdata in ancil:
125                 if clvl == socket.SOL_SOCKET and ctype == socket.SCM_RIGHTS:
126                     got_fds += cdata # need to trim any surplus, and unpack
127
128         finally:
129             if s is not None: s.close()
130             if s2 is not None: s2.close()
131
132             try: os.remove(our_sockname)
133             except FileNotFoundError: pass