chiark / gitweb /
c538baa846372d67f47db1b55680c7567bea973e
[chiark-utils.git] / fishdescriptor / py / fishdescriptor / indonor.py
1
2 # class for use inside gdb which is debugging the donor process
3
4 import gdb
5 import copy
6 import os
7 import sys
8 import socket
9
10 def _string_bytearray(s):
11     # gets us bytes in py2 and py3
12     if not isinstance(s, bytes):
13         s = s.encode('utf-8') # sigh, python 2/3 compat
14     return bytearray(s)
15
16 def _string_escape_for_c(s):
17     out = ''
18     for c in _string_bytearray(s):
19         if c == ord('\\') or c == ord('"') or c < 32 or c > 126:
20             out += '\\x%02x' % c
21         else:
22             out += chr(c)
23     return out
24
25 # constructing values
26
27 def _lit_integer(v):
28     return '%d' % v
29
30 def _lit_aggregate_uncasted(val_lit_strs):
31     return '{' + ', '.join(['(%s)' % v for v in val_lit_strs]) + ' }'
32
33 def _lit_string_uncasted(s):
34     b = _string_bytearray(s)
35     return _lit_aggregate_uncasted([_lit_integer(x) for x in b] + [ '0' ])
36
37 def _lit_array(elemtype, val_lit_strs):
38     return (
39         '((%s[%d])%s)' %
40         (elemtype, len(val_lit_strs), _lit_aggregate_uncasted(val_lit_strs))
41     )
42
43 def _lit_addressof(v):
44     return '&(char[])(%s)' % v
45
46 def _make_lit(v):
47     if isinstance(v, int):
48         return _lit_integer(v)
49     else:
50         return v # should already be an integer
51
52 def parse_eval(expr):
53     sys.stderr.write("##  EVAL %s\n" % repr(expr))
54     x = gdb.parse_and_eval(expr)
55     sys.stderr.write('##  => %s\n' % x)
56     sys.stderr.flush()
57     return x
58
59 class DonorStructLayout():
60     def __init__(l, typename):
61         x = gdb.lookup_type(typename)
62         l._typename = typename
63         l._template = [ ]
64         l._posns = { }
65         for f in x.fields():
66             l._posns[f.name] = len(l._template)
67             try: f.type.fields();  blank = '{ }'
68             except TypeError:      blank = '0'
69             except AttributeError: blank = '0'
70             l._template.append(blank)
71         sys.stderr.write('##  STRUCT %s template %s fields %s\n'
72                          % (typename, l._template, l._posns))
73
74     def substitute(l, values):
75         build = copy.deepcopy(l._template)
76         for (k,v) in values.items():
77             build[ l._posns[k] ] = _make_lit(v)
78         return '((%s)%s)' % (l._typename, _lit_aggregate_uncasted(build))
79
80 class DonorImplementation():
81     def __init__(di):
82         di._structs = { }
83         di._saved_errno = None
84         di._result_stream = os.fdopen(3, 'w')
85         di._errno_workaround = None
86
87     # assembling structs
88     # sigh, we have to record the order of the arguments!
89     def _find_fields(di, typename):
90         try:
91             fields = di._structs[typename]
92         except KeyError:
93             fields = DonorStructLayout(typename)
94             di._structs[typename] = fields
95         return fields
96
97     def _make(di, typename, values):
98         fields = di._find_fields(typename)
99         return fields.substitute(values)
100
101     # hideous workaround
102
103     def _parse_eval_errno(di, expr_pat):
104         # evaluates  expr_pat % 'errno'
105         if di._errno_workaround is not True:
106             try:
107                 x = parse_eval(expr_pat % 'errno')
108                 di._errno_workaround = False
109                 return x
110             except gdb.error as e:
111                 if di._errno_workaround is False:
112                     raise e
113             di._errno_workaround = True
114         # Incomprehensibly, gdb.parse_and_eval('errno') can sometimes
115         # fail with
116         #   gdb.error: Cannot find thread-local variables on this target
117         # even though plain gdb `print errno' works while `print errno = 25'
118         # doesn't.   OMG.  This may be related to:
119         #  https://github.com/cloudburst/libheap/issues/24
120         # although I can't find it in the gdb bug db (which is half-broken
121         # in my browser).  Also the error is very nonspecific :-/.
122         # This seems to happen on jessie, and is fixed in stretch.
123         # Anyway:
124         return parse_eval(expr_pat % '(*((int (*)(void))__errno_location)())')
125
126     # calling functions (need to cast the function name to the right
127     # type in case maybe gdb doesn't know the type)
128
129     def _func(di, functype, funcname, realargs):
130         expr = '((%s) %s) %s' % (functype, funcname, realargs)
131         return parse_eval(expr)
132
133     def _must_func(di, functype, funcname, realargs):
134         retval = di._func(functype, funcname, realargs)
135         if retval < 0:
136             errnoval = di._parse_eval_errno('%s')
137             raise RuntimeError("%s gave errno=%d `%s'" %
138                                (funcname, errnoval, os.strerror(errnoval)))
139         return retval
140
141     # wrappers for the syscalls that do what we want
142
143     def _sendmsg(di, carrier, control_msg):
144         iov_base = _lit_array('char', [1])
145         iov = di._make('struct iovec', {
146             'iov_base': iov_base,
147             'iov_len' : 1,
148         })
149
150         msg = di._make('struct msghdr', {
151             'msg_iov'       : _lit_addressof(iov),
152             'msg_iovlen'    : 1,
153             'msg_control'   : _lit_array('char', control_msg),
154             'msg_controllen': len(control_msg),
155         })
156
157         di._must_func(
158             'ssize_t (*)(int, const struct msghdr*, int)',
159             'sendmsg',
160             '(%s, %s, 0)' % (carrier, _lit_addressof(msg))
161         )
162
163     def _socket(di):
164         return di._must_func(
165             'int (*)(int, int, int)',
166             'socket',
167             '(%d, %d, 0)' % (socket.AF_UNIX, socket.SOCK_STREAM)
168         )
169
170     def _connect(di, fd, path):
171         addr = di._make('struct sockaddr_un', {
172             'sun_family' : _lit_integer(socket.AF_UNIX),
173             'sun_path'   : _lit_string_uncasted(path),
174         })
175
176         di._must_func(
177             'int (*)(int, const struct sockaddr*, socklen_t)',
178             'connect',
179             '(%d, (const struct sockaddr*)%s, sizeof(struct sockaddr_un))'
180             % (fd, _lit_addressof(addr))
181         )
182
183     def _close(di, fd):
184         di._must_func('int (*)(int)', 'close', '(%d)' % fd)
185
186     def _mkdir(di, path, mode):
187         r = di._func(
188             'int (*)(const char*, mode_t)',
189             'mkdir',
190             '("%s", %d)' % (_string_escape_for_c(path), mode)
191         )
192         if r < 0:
193             errnoval = di._parse_eval_errno('%s')
194             if errnoval != os.errno.EEXIST:
195                 raise RuntimeError("mkdir %s failed: `%s'" %
196                                    (repr(path), os.strerror(errnoval)))
197             return 0
198         return 1
199
200     def _errno_save(di):
201         di._saved_errno = di._parse_eval_errno('%s')
202
203     def _errno_restore(di):
204         to_restore = di._saved_errno
205         di._saved_errno = None
206         if to_restore is not None:
207             di._parse_eval_errno('%%s = %d' % to_restore)
208
209     def _result(di, output):
210         sys.stderr.write("#> %s" % output)
211         di._result_stream.write(output)
212         di._result_stream.flush()
213
214     # main entrypoints
215
216     def donate(di, path, control_msg):
217         # control_msg is an array of integers being the ancillary data
218         # array ("control") for sendmsg, and hence specifies which fds
219         # to pass
220
221         carrier = None
222         try:
223             di._errno_save()
224             carrier = di._socket()
225             di._connect(carrier, path)
226             di._sendmsg(carrier, control_msg)
227             di._close(carrier)
228             carrier = None
229         finally:
230             if carrier is not None:
231                 try: di._close(carrier)
232                 except Exception: pass
233             di._errno_restore()
234
235         di._result('1\n')
236
237     def geteuid(di):
238         try:
239             di._errno_save()
240             val = di._must_func('uid_t (*)(void)', 'geteuid', '()')
241         finally:
242             di._errno_restore()
243         
244         di._result('%d\n' % val)
245
246     def mkdir(di, path):
247         try:
248             di._errno_save()
249             val = di._mkdir(path, int('0700', 8))
250         finally:
251             di._errno_restore()
252
253         di._result('%d\n' % val)
254
255     def _protocol_read(di):
256         input = sys.stdin.readline()
257         if input == '': return None
258         input = input.rstrip('\n')
259         sys.stderr.write("#< %s\n" % input)
260         return input
261
262     def eval_loop(di):
263         while True:
264             di._result('!\n')
265             cmd = di._protocol_read()
266             if cmd is None: break
267             eval(cmd)