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