xref: /llvm-project/lldb/packages/Python/lldbsuite/test/gdbclientutils.py (revision b7b9ccf44988edf49886743ae5c3cf4184db211f)
1import ctypes
2import errno
3import io
4import threading
5import socket
6import traceback
7from lldbsuite.support import seven
8
9
10def checksum(message):
11    """
12    Calculate the GDB server protocol checksum of the message.
13
14    The GDB server protocol uses a simple modulo 256 sum.
15    """
16    check = 0
17    for c in message:
18        check += ord(c)
19    return check % 256
20
21
22def frame_packet(message):
23    """
24    Create a framed packet that's ready to send over the GDB connection
25    channel.
26
27    Framing includes surrounding the message between $ and #, and appending
28    a two character hex checksum.
29    """
30    return "$%s#%02x" % (message, checksum(message))
31
32
33def escape_binary(message):
34    """
35    Escape the binary message using the process described in the GDB server
36    protocol documentation.
37
38    Most bytes are sent through as-is, but $, #, and { are escaped by writing
39    a { followed by the original byte mod 0x20.
40    """
41    out = ""
42    for c in message:
43        d = ord(c)
44        if d in (0x23, 0x24, 0x7D):
45            out += chr(0x7D)
46            out += chr(d ^ 0x20)
47        else:
48            out += c
49    return out
50
51
52def hex_encode_bytes(message):
53    """
54    Encode the binary message by converting each byte into a two-character
55    hex string.
56    """
57    out = ""
58    for c in message:
59        out += "%02x" % ord(c)
60    return out
61
62
63def hex_decode_bytes(hex_bytes):
64    """
65    Decode the hex string into a binary message by converting each two-character
66    hex string into a single output byte.
67    """
68    out = ""
69    hex_len = len(hex_bytes)
70    i = 0
71    while i < hex_len - 1:
72        out += chr(int(hex_bytes[i : i + 2], 16))
73        i += 2
74    return out
75
76
77class MockGDBServerResponder:
78    """
79    A base class for handling client packets and issuing server responses for
80    GDB tests.
81
82    This handles many typical situations, while still allowing subclasses to
83    completely customize their responses.
84
85    Most subclasses will be interested in overriding the other() method, which
86    handles any packet not recognized in the common packet handling code.
87    """
88
89    registerCount = 40
90    packetLog = None
91
92    class RESPONSE_DISCONNECT:
93        pass
94
95    def __init__(self):
96        self.packetLog = []
97
98    def respond(self, packet):
99        """
100        Return the unframed packet data that the server should issue in response
101        to the given packet received from the client.
102        """
103        self.packetLog.append(packet)
104        if packet is MockGDBServer.PACKET_INTERRUPT:
105            return self.interrupt()
106        if packet == "c":
107            return self.cont()
108        if packet.startswith("vCont;c"):
109            return self.vCont(packet)
110        if packet[0] == "A":
111            return self.A(packet)
112        if packet[0] == "D":
113            return self.D(packet)
114        if packet[0] == "g":
115            return self.readRegisters()
116        if packet[0] == "G":
117            # Gxxxxxxxxxxx
118            # Gxxxxxxxxxxx;thread:1234;
119            return self.writeRegisters(packet[1:].split(";")[0])
120        if packet[0] == "p":
121            regnum = packet[1:].split(";")[0]
122            return self.readRegister(int(regnum, 16))
123        if packet[0] == "P":
124            register, value = packet[1:].split("=")
125            return self.writeRegister(int(register, 16), value)
126        if packet[0] == "m":
127            addr, length = [int(x, 16) for x in packet[1:].split(",")]
128            return self.readMemory(addr, length)
129        if packet[0] == "M":
130            location, encoded_data = packet[1:].split(":")
131            addr, length = [int(x, 16) for x in location.split(",")]
132            return self.writeMemory(addr, encoded_data)
133        if packet[0:7] == "qSymbol":
134            return self.qSymbol(packet[8:])
135        if packet[0:10] == "qSupported":
136            return self.qSupported(packet[11:].split(";"))
137        if packet == "qfThreadInfo":
138            return self.qfThreadInfo()
139        if packet == "qsThreadInfo":
140            return self.qsThreadInfo()
141        if packet == "qC":
142            return self.qC()
143        if packet == "QEnableErrorStrings":
144            return self.QEnableErrorStrings()
145        if packet == "?":
146            return self.haltReason()
147        if packet == "s":
148            return self.haltReason()
149        if packet[0] == "H":
150            tid = packet[2:]
151            if "." in tid:
152                assert tid.startswith("p")
153                # TODO: do we want to do anything with PID?
154                tid = tid.split(".", 1)[1]
155            return self.selectThread(packet[1], int(tid, 16))
156        if packet[0:6] == "qXfer:":
157            obj, read, annex, location = packet[6:].split(":")
158            offset, length = [int(x, 16) for x in location.split(",")]
159            data, has_more = self.qXferRead(obj, annex, offset, length)
160            if data is not None:
161                return self._qXferResponse(data, has_more)
162            return ""
163        if packet.startswith("vAttach;"):
164            pid = packet.partition(";")[2]
165            return self.vAttach(int(pid, 16))
166        if packet[0] == "Z":
167            return self.setBreakpoint(packet)
168        if packet.startswith("qThreadStopInfo"):
169            threadnum = int(packet[15:], 16)
170            return self.threadStopInfo(threadnum)
171        if packet == "QThreadSuffixSupported":
172            return self.QThreadSuffixSupported()
173        if packet == "QListThreadsInStopReply":
174            return self.QListThreadsInStopReply()
175        if packet.startswith("qMemoryRegionInfo:"):
176            return self.qMemoryRegionInfo(int(packet.split(":")[1], 16))
177        if packet == "qQueryGDBServer":
178            return self.qQueryGDBServer()
179        if packet == "qHostInfo":
180            return self.qHostInfo()
181        if packet == "qGetWorkingDir":
182            return self.qGetWorkingDir()
183        if packet == "qOffsets":
184            return self.qOffsets()
185        if packet == "qProcessInfo":
186            return self.qProcessInfo()
187        if packet == "qsProcessInfo":
188            return self.qsProcessInfo()
189        if packet.startswith("qfProcessInfo"):
190            return self.qfProcessInfo(packet)
191        if packet.startswith("jGetLoadedDynamicLibrariesInfos"):
192            return self.jGetLoadedDynamicLibrariesInfos(packet)
193        if packet.startswith("qPathComplete:"):
194            return self.qPathComplete()
195        if packet.startswith("vFile:"):
196            return self.vFile(packet)
197        if packet.startswith("vRun;"):
198            return self.vRun(packet)
199        if packet.startswith("qLaunchGDBServer;"):
200            _, host = packet.partition(";")[2].split(":")
201            return self.qLaunchGDBServer(host)
202        if packet.startswith("qLaunchSuccess"):
203            return self.qLaunchSuccess()
204        if packet.startswith("QEnvironment:"):
205            return self.QEnvironment(packet)
206        if packet.startswith("QEnvironmentHexEncoded:"):
207            return self.QEnvironmentHexEncoded(packet)
208        if packet.startswith("qRegisterInfo"):
209            regnum = int(packet[len("qRegisterInfo") :], 16)
210            return self.qRegisterInfo(regnum)
211        if packet == "k":
212            return self.k()
213
214        return self.other(packet)
215
216    def qsProcessInfo(self):
217        return "E04"
218
219    def qfProcessInfo(self, packet):
220        return "E04"
221
222    def jGetLoadedDynamicLibrariesInfos(self, packet):
223        return ""
224
225    def qGetWorkingDir(self):
226        return "2f"
227
228    def qOffsets(self):
229        return ""
230
231    def qProcessInfo(self):
232        return ""
233
234    def qHostInfo(self):
235        return "ptrsize:8;endian:little;"
236
237    def qQueryGDBServer(self):
238        return "E04"
239
240    def interrupt(self):
241        raise self.UnexpectedPacketException()
242
243    def cont(self):
244        raise self.UnexpectedPacketException()
245
246    def vCont(self, packet):
247        raise self.UnexpectedPacketException()
248
249    def A(self, packet):
250        return ""
251
252    def D(self, packet):
253        return "OK"
254
255    def readRegisters(self):
256        return "00000000" * self.registerCount
257
258    def readRegister(self, register):
259        return "00000000"
260
261    def writeRegisters(self, registers_hex):
262        return "OK"
263
264    def writeRegister(self, register, value_hex):
265        return "OK"
266
267    def readMemory(self, addr, length):
268        return "00" * length
269
270    def writeMemory(self, addr, data_hex):
271        return "OK"
272
273    def qSymbol(self, symbol_args):
274        return "OK"
275
276    def qSupported(self, client_supported):
277        return "qXfer:features:read+;PacketSize=3fff;QStartNoAckMode+"
278
279    def qfThreadInfo(self):
280        return "l"
281
282    def qsThreadInfo(self):
283        return "l"
284
285    def qC(self):
286        return "QC0"
287
288    def QEnableErrorStrings(self):
289        return "OK"
290
291    def haltReason(self):
292        # SIGINT is 2, return type is 2 digit hex string
293        return "S02"
294
295    def qXferRead(self, obj, annex, offset, length):
296        return None, False
297
298    def _qXferResponse(self, data, has_more):
299        return "%s%s" % ("m" if has_more else "l", escape_binary(data))
300
301    def vAttach(self, pid):
302        raise self.UnexpectedPacketException()
303
304    def selectThread(self, op, thread_id):
305        return "OK"
306
307    def setBreakpoint(self, packet):
308        raise self.UnexpectedPacketException()
309
310    def threadStopInfo(self, threadnum):
311        return ""
312
313    def other(self, packet):
314        # empty string means unsupported
315        return ""
316
317    def QThreadSuffixSupported(self):
318        return ""
319
320    def QListThreadsInStopReply(self):
321        return ""
322
323    def qMemoryRegionInfo(self, addr):
324        return ""
325
326    def qPathComplete(self):
327        return ""
328
329    def vFile(self, packet):
330        return ""
331
332    def vRun(self, packet):
333        return ""
334
335    def qLaunchGDBServer(self, host):
336        raise self.UnexpectedPacketException()
337
338    def qLaunchSuccess(self):
339        return ""
340
341    def QEnvironment(self, packet):
342        return "OK"
343
344    def QEnvironmentHexEncoded(self, packet):
345        return "OK"
346
347    def qRegisterInfo(self, num):
348        return ""
349
350    def k(self):
351        return ["W01", self.RESPONSE_DISCONNECT]
352
353    """
354    Raised when we receive a packet for which there is no default action.
355    Override the responder class to implement behavior suitable for the test at
356    hand.
357    """
358
359    class UnexpectedPacketException(Exception):
360        pass
361
362
363class ServerChannel:
364    """
365    A wrapper class for TCP or pty-based server.
366    """
367
368    def get_connect_address(self):
369        """Get address for the client to connect to."""
370
371    def get_connect_url(self):
372        """Get URL suitable for process connect command."""
373
374    def close_server(self):
375        """Close all resources used by the server."""
376
377    def accept(self):
378        """Accept a single client connection to the server."""
379
380    def close_connection(self):
381        """Close all resources used by the accepted connection."""
382
383    def recv(self):
384        """Receive a data packet from the connected client."""
385
386    def sendall(self, data):
387        """Send the data to the connected client."""
388
389
390class ServerSocket(ServerChannel):
391    def __init__(self, family, type, proto, addr):
392        self._server_socket = socket.socket(family, type, proto)
393        self._connection = None
394
395        self._server_socket.bind(addr)
396        self._server_socket.listen(1)
397
398    def close_server(self):
399        self._server_socket.close()
400
401    def accept(self):
402        assert self._connection is None
403        # accept() is stubborn and won't fail even when the socket is
404        # shutdown, so we'll use a timeout
405        self._server_socket.settimeout(30.0)
406        client, client_addr = self._server_socket.accept()
407        # The connected client inherits its timeout from self._socket,
408        # but we'll use a blocking socket for the client
409        client.settimeout(None)
410        self._connection = client
411
412    def close_connection(self):
413        assert self._connection is not None
414        self._connection.close()
415        self._connection = None
416
417    def recv(self):
418        assert self._connection is not None
419        return self._connection.recv(4096)
420
421    def sendall(self, data):
422        assert self._connection is not None
423        return self._connection.sendall(data)
424
425
426class TCPServerSocket(ServerSocket):
427    def __init__(self):
428        family, type, proto, _, addr = socket.getaddrinfo(
429            "localhost", 0, proto=socket.IPPROTO_TCP
430        )[0]
431        super().__init__(family, type, proto, addr)
432
433    def get_connect_address(self):
434        return "[{}]:{}".format(*self._server_socket.getsockname())
435
436    def get_connect_url(self):
437        return "connect://" + self.get_connect_address()
438
439
440class UnixServerSocket(ServerSocket):
441    def __init__(self, addr):
442        super().__init__(socket.AF_UNIX, socket.SOCK_STREAM, 0, addr)
443
444    def get_connect_address(self):
445        return self._server_socket.getsockname()
446
447    def get_connect_url(self):
448        return "unix-connect://" + self.get_connect_address()
449
450
451class PtyServerSocket(ServerChannel):
452    def __init__(self):
453        import pty
454        import tty
455
456        primary, secondary = pty.openpty()
457        tty.setraw(primary)
458        self._primary = io.FileIO(primary, "r+b")
459        self._secondary = io.FileIO(secondary, "r+b")
460
461    def get_connect_address(self):
462        libc = ctypes.CDLL(None)
463        libc.ptsname.argtypes = (ctypes.c_int,)
464        libc.ptsname.restype = ctypes.c_char_p
465        return libc.ptsname(self._primary.fileno()).decode()
466
467    def get_connect_url(self):
468        return "serial://" + self.get_connect_address()
469
470    def close_server(self):
471        self._secondary.close()
472        self._primary.close()
473
474    def recv(self):
475        try:
476            return self._primary.read(4096)
477        except OSError as e:
478            # closing the pty results in EIO on Linux, convert it to EOF
479            if e.errno == errno.EIO:
480                return b""
481            raise
482
483    def sendall(self, data):
484        return self._primary.write(data)
485
486
487class MockGDBServer:
488    """
489    A simple TCP-based GDB server that can test client behavior by receiving
490    commands and issuing custom-tailored responses.
491
492    Responses are generated via the .responder property, which should be an
493    instance of a class based on MockGDBServerResponder.
494    """
495
496    responder = None
497    _socket = None
498    _thread = None
499    _receivedData = None
500    _receivedDataOffset = None
501    _shouldSendAck = True
502
503    def __init__(self, socket):
504        self._socket = socket
505        self.responder = MockGDBServerResponder()
506
507    def start(self):
508        # Start a thread that waits for a client connection.
509        self._thread = threading.Thread(target=self.run)
510        self._thread.start()
511
512    def stop(self):
513        if self._thread is not None:
514            self._thread.join()
515            self._thread = None
516
517    def get_connect_address(self):
518        return self._socket.get_connect_address()
519
520    def get_connect_url(self):
521        return self._socket.get_connect_url()
522
523    def run(self):
524        # For testing purposes, we only need to worry about one client
525        # connecting just one time.
526        try:
527            self._socket.accept()
528        except:
529            traceback.print_exc()
530            return
531        self._shouldSendAck = True
532        self._receivedData = ""
533        self._receivedDataOffset = 0
534        data = None
535        try:
536            while True:
537                data = seven.bitcast_to_string(self._socket.recv())
538                if data is None or len(data) == 0:
539                    break
540                self._receive(data)
541        except self.TerminateConnectionException:
542            pass
543        except Exception as e:
544            print(
545                "An exception happened when receiving the response from the gdb server. Closing the client..."
546            )
547            traceback.print_exc()
548        finally:
549            self._socket.close_connection()
550            self._socket.close_server()
551
552    def _receive(self, data):
553        """
554        Collects data, parses and responds to as many packets as exist.
555        Any leftover data is kept for parsing the next time around.
556        """
557        self._receivedData += data
558        packet = self._parsePacket()
559        while packet is not None:
560            self._handlePacket(packet)
561            packet = self._parsePacket()
562
563    def _parsePacket(self):
564        """
565        Reads bytes from self._receivedData, returning:
566        - a packet's contents if a valid packet is found
567        - the PACKET_ACK unique object if we got an ack
568        - None if we only have a partial packet
569
570        Raises an InvalidPacketException if unexpected data is received
571        or if checksums fail.
572
573        Once a complete packet is found at the front of self._receivedData,
574        its data is removed form self._receivedData.
575        """
576        data = self._receivedData
577        i = self._receivedDataOffset
578        data_len = len(data)
579        if data_len == 0:
580            return None
581        if i == 0:
582            # If we're looking at the start of the received data, that means
583            # we're looking for the start of a new packet, denoted by a $.
584            # It's also possible we'll see an ACK here, denoted by a +
585            if data[0] == "+":
586                self._receivedData = data[1:]
587                return self.PACKET_ACK
588            if ord(data[0]) == 3:
589                self._receivedData = data[1:]
590                return self.PACKET_INTERRUPT
591            if data[0] == "$":
592                i += 1
593            else:
594                raise self.InvalidPacketException(
595                    "Unexpected leading byte: %s" % data[0]
596                )
597
598        # If we're looking beyond the start of the received data, then we're
599        # looking for the end of the packet content, denoted by a #.
600        # Note that we pick up searching from where we left off last time
601        while i < data_len and data[i] != "#":
602            i += 1
603
604        # If there isn't enough data left for a checksum, just remember where
605        # we left off so we can pick up there the next time around
606        if i > data_len - 3:
607            self._receivedDataOffset = i
608            return None
609
610        # If we have enough data remaining for the checksum, extract it and
611        # compare to the packet contents
612        packet = data[1:i]
613        i += 1
614        try:
615            check = int(data[i : i + 2], 16)
616        except ValueError:
617            raise self.InvalidPacketException("Checksum is not valid hex")
618        i += 2
619        if check != checksum(packet):
620            raise self.InvalidPacketException(
621                "Checksum %02x does not match content %02x" % (check, checksum(packet))
622            )
623        # remove parsed bytes from _receivedData and reset offset so parsing
624        # can start on the next packet the next time around
625        self._receivedData = data[i:]
626        self._receivedDataOffset = 0
627        return packet
628
629    def _sendPacket(self, packet):
630        self._socket.sendall(seven.bitcast_to_bytes(frame_packet(packet)))
631
632    def _handlePacket(self, packet):
633        if packet is self.PACKET_ACK:
634            # Ignore ACKs from the client. For the future, we can consider
635            # adding validation code to make sure the client only sends ACKs
636            # when it's supposed to.
637            return
638        response = ""
639        # We'll handle the ack stuff here since it's not something any of the
640        # tests will be concerned about, and it'll get turned off quickly anyway.
641        if self._shouldSendAck:
642            self._socket.sendall(seven.bitcast_to_bytes("+"))
643        if packet == "QStartNoAckMode":
644            self._shouldSendAck = False
645            response = "OK"
646        elif self.responder is not None:
647            # Delegate everything else to our responder
648            response = self.responder.respond(packet)
649        if not isinstance(response, list):
650            response = [response]
651        for part in response:
652            if part is MockGDBServerResponder.RESPONSE_DISCONNECT:
653                raise self.TerminateConnectionException()
654            self._sendPacket(part)
655
656    PACKET_ACK = object()
657    PACKET_INTERRUPT = object()
658
659    class TerminateConnectionException(Exception):
660        pass
661
662    class InvalidPacketException(Exception):
663        pass
664