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