741 lines · python
1from abc import ABC, abstractmethod2import ctypes3import errno4import io5import threading6import socket7import traceback8from enum import Enum9from lldbsuite.support import seven10from typing import Optional, List, Tuple, Union, Sequence11 12 13def checksum(message):14 """15 Calculate the GDB server protocol checksum of the message.16 17 The GDB server protocol uses a simple modulo 256 sum.18 """19 check = 020 for c in message:21 check += ord(c)22 return check % 25623 24 25def frame_packet(message):26 """27 Create a framed packet that's ready to send over the GDB connection28 channel.29 30 Framing includes surrounding the message between $ and #, and appending31 a two character hex checksum.32 """33 return "$%s#%02x" % (message, checksum(message))34 35 36def escape_binary(message):37 """38 Escape the binary message using the process described in the GDB server39 protocol documentation.40 41 Most bytes are sent through as-is, but $, #, and { are escaped by writing42 a { followed by the original byte mod 0x20.43 """44 out = ""45 for c in message:46 d = ord(c)47 if d in (0x23, 0x24, 0x7D):48 out += chr(0x7D)49 out += chr(d ^ 0x20)50 else:51 out += c52 return out53 54 55def hex_encode_bytes(message):56 """57 Encode the binary message by converting each byte into a two-character58 hex string.59 """60 out = ""61 for c in message:62 out += "%02x" % ord(c)63 return out64 65 66def hex_decode_bytes(hex_bytes):67 """68 Decode the hex string into a binary message by converting each two-character69 hex string into a single output byte.70 """71 out = ""72 hex_len = len(hex_bytes)73 i = 074 while i < hex_len - 1:75 out += chr(int(hex_bytes[i : i + 2], 16))76 i += 277 return out78 79 80class PacketDirection(Enum):81 RECV = "recv"82 SEND = "send"83 84 85class PacketLog:86 def __init__(self):87 self._packets: list[tuple[PacketDirection, str]] = []88 89 def add_sent(self, packet: str):90 self._packets.append((PacketDirection.SEND, packet))91 92 def add_received(self, packet: str):93 self._packets.append((PacketDirection.RECV, packet))94 95 def get_sent(self):96 return [97 pkt for direction, pkt in self._packets if direction == PacketDirection.SEND98 ]99 100 def get_received(self):101 return [102 pkt for direction, pkt in self._packets if direction == PacketDirection.RECV103 ]104 105 def __iter__(self):106 return iter(self._packets)107 108 109class MockGDBServerResponder:110 """111 A base class for handling client packets and issuing server responses for112 GDB tests.113 114 This handles many typical situations, while still allowing subclasses to115 completely customize their responses.116 117 Most subclasses will be interested in overriding the other() method, which118 handles any packet not recognized in the common packet handling code.119 """120 121 registerCount: int = 40122 123 class SpecialResponse(Enum):124 RESPONSE_DISCONNECT = 0125 RESPONSE_NONE = 1126 127 RESPONSE_DISCONNECT = SpecialResponse.RESPONSE_DISCONNECT128 RESPONSE_NONE = SpecialResponse.RESPONSE_NONE129 Response = Union[str, SpecialResponse]130 131 def __init__(self):132 self.packetLog = PacketLog()133 134 def respond(self, packet: str) -> Sequence[Response]:135 """136 Return the unframed packet data that the server should issue in response137 to the given packet received from the client.138 """139 self.packetLog.add_received(packet)140 response = self._respond_impl(packet)141 if not isinstance(response, list):142 response = [response]143 for part in response:144 if isinstance(part, self.SpecialResponse):145 continue146 self.packetLog.add_sent(part)147 return response148 149 def _respond_impl(self, packet) -> Union[Response, List[Response]]:150 if packet is MockGDBServer.PACKET_INTERRUPT:151 return self.interrupt()152 if packet == "c":153 return self.cont()154 if packet.startswith("vCont;c"):155 return self.vCont(packet)156 if packet[0] == "A":157 return self.A(packet)158 if packet[0] == "D":159 return self.D(packet)160 if packet[0] == "g":161 return self.readRegisters()162 if packet[0] == "G":163 # Gxxxxxxxxxxx164 # Gxxxxxxxxxxx;thread:1234;165 return self.writeRegisters(packet[1:].split(";")[0])166 if packet[0] == "p":167 regnum = packet[1:].split(";")[0]168 return self.readRegister(int(regnum, 16))169 if packet[0] == "P":170 register, value = packet[1:].split("=")171 return self.writeRegister(int(register, 16), value)172 if packet[0] == "m":173 addr, length = [int(x, 16) for x in packet[1:].split(",")]174 return self.readMemory(addr, length)175 if packet[0] == "x":176 addr, length = [int(x, 16) for x in packet[1:].split(",")]177 return self.x(addr, length)178 if packet[0] == "M":179 location, encoded_data = packet[1:].split(":")180 addr, length = [int(x, 16) for x in location.split(",")]181 return self.writeMemory(addr, encoded_data)182 if packet[0:7] == "qSymbol":183 return self.qSymbol(packet[8:])184 if packet[0:10] == "qSupported":185 return self.qSupported(packet[11:].split(";"))186 if packet == "qfThreadInfo":187 return self.qfThreadInfo()188 if packet == "qsThreadInfo":189 return self.qsThreadInfo()190 if packet == "qC":191 return self.qC()192 if packet == "QEnableErrorStrings":193 return self.QEnableErrorStrings()194 if packet == "?":195 return self.haltReason()196 if packet == "s":197 return self.haltReason()198 if packet[0] == "H":199 tid = packet[2:]200 if "." in tid:201 assert tid.startswith("p")202 # TODO: do we want to do anything with PID?203 tid = tid.split(".", 1)[1]204 return self.selectThread(packet[1], int(tid, 16))205 if packet[0:6] == "qXfer:":206 obj, read, annex, location = packet[6:].split(":")207 offset, length = [int(x, 16) for x in location.split(",")]208 data, has_more = self.qXferRead(obj, annex, offset, length)209 if data is not None:210 return self._qXferResponse(data, has_more)211 return ""212 if packet.startswith("vAttach;"):213 pid = packet.partition(";")[2]214 return self.vAttach(int(pid, 16))215 if packet[0] == "Z":216 return self.setBreakpoint(packet)217 if packet.startswith("qThreadStopInfo"):218 threadnum = int(packet[15:], 16)219 return self.threadStopInfo(threadnum)220 if packet == "QThreadSuffixSupported":221 return self.QThreadSuffixSupported()222 if packet == "QListThreadsInStopReply":223 return self.QListThreadsInStopReply()224 if packet.startswith("qMemoryRegionInfo:"):225 return self.qMemoryRegionInfo(int(packet.split(":")[1], 16))226 if packet == "qQueryGDBServer":227 return self.qQueryGDBServer()228 if packet == "qHostInfo":229 return self.qHostInfo()230 if packet.startswith("qEcho"):231 return self.qEcho(int(packet.split(":")[1]))232 if packet == "qGetWorkingDir":233 return self.qGetWorkingDir()234 if packet == "qOffsets":235 return self.qOffsets()236 if packet == "qProcessInfo":237 return self.qProcessInfo()238 if packet == "qsProcessInfo":239 return self.qsProcessInfo()240 if packet.startswith("qfProcessInfo"):241 return self.qfProcessInfo(packet)242 if packet.startswith("jGetLoadedDynamicLibrariesInfos"):243 return self.jGetLoadedDynamicLibrariesInfos(packet)244 if packet.startswith("qPathComplete:"):245 return self.qPathComplete()246 if packet.startswith("vFile:"):247 return self.vFile(packet)248 if packet.startswith("vRun;"):249 return self.vRun(packet)250 if packet.startswith("qLaunchGDBServer;"):251 _, host = packet.partition(";")[2].split(":")252 return self.qLaunchGDBServer(host)253 if packet.startswith("qLaunchSuccess"):254 return self.qLaunchSuccess()255 if packet.startswith("QEnvironment:"):256 return self.QEnvironment(packet)257 if packet.startswith("QEnvironmentHexEncoded:"):258 return self.QEnvironmentHexEncoded(packet)259 if packet.startswith("qRegisterInfo"):260 regnum = int(packet[len("qRegisterInfo") :], 16)261 return self.qRegisterInfo(regnum)262 if packet == "k":263 return self.k()264 265 return self.other(packet)266 267 def qsProcessInfo(self) -> str:268 return "E04"269 270 def qfProcessInfo(self, packet) -> str:271 return "E04"272 273 def jGetLoadedDynamicLibrariesInfos(self, packet) -> str:274 return ""275 276 def qGetWorkingDir(self) -> str:277 return "2f"278 279 def qOffsets(self) -> str:280 return ""281 282 def qProcessInfo(self) -> str:283 return ""284 285 def qHostInfo(self) -> str:286 return "ptrsize:8;endian:little;"287 288 def qEcho(self, num: int) -> str:289 return "E04"290 291 def qQueryGDBServer(self) -> str:292 return "E04"293 294 def interrupt(self):295 raise self.UnexpectedPacketException()296 297 def cont(self):298 raise self.UnexpectedPacketException()299 300 def vCont(self, packet):301 raise self.UnexpectedPacketException()302 303 def A(self, packet) -> str:304 return ""305 306 def D(self, packet) -> str:307 return "OK"308 309 def readRegisters(self) -> str:310 return "00000000" * self.registerCount311 312 def readRegister(self, register: int) -> str:313 return "00000000"314 315 def writeRegisters(self, registers_hex) -> str:316 return "OK"317 318 def writeRegister(self, register, value_hex) -> str:319 return "OK"320 321 def readMemory(self, addr, length) -> str:322 return "00" * length323 324 def x(self, addr, length) -> str:325 return ""326 327 def writeMemory(self, addr, data_hex) -> str:328 return "OK"329 330 def qSymbol(self, symbol_args) -> str:331 return "OK"332 333 def qSupported(self, client_supported) -> str:334 return "qXfer:features:read+;PacketSize=3fff;QStartNoAckMode+"335 336 def qfThreadInfo(self) -> str:337 return "l"338 339 def qsThreadInfo(self) -> str:340 return "l"341 342 def qC(self) -> str:343 return "QC0"344 345 def QEnableErrorStrings(self) -> str:346 return "OK"347 348 def haltReason(self) -> str:349 # SIGINT is 2, return type is 2 digit hex string350 return "S02"351 352 def qXferRead(353 self, obj: str, annex: str, offset: int, length: int354 ) -> Tuple[Optional[str], bool]:355 return None, False356 357 def _qXferResponse(self, data, has_more):358 return "%s%s" % ("m" if has_more else "l", escape_binary(data))359 360 def vAttach(self, pid):361 raise self.UnexpectedPacketException()362 363 def selectThread(self, op, thread_id) -> str:364 return "OK"365 366 def setBreakpoint(self, packet) -> str:367 raise self.UnexpectedPacketException()368 369 def threadStopInfo(self, threadnum) -> str:370 return ""371 372 def other(self, packet) -> str:373 # empty string means unsupported374 return ""375 376 def QThreadSuffixSupported(self) -> str:377 return ""378 379 def QListThreadsInStopReply(self) -> str:380 return ""381 382 def qMemoryRegionInfo(self, addr) -> str:383 return ""384 385 def qPathComplete(self) -> str:386 return ""387 388 def vFile(self, packet) -> str:389 return ""390 391 def vRun(self, packet) -> str:392 return ""393 394 def qLaunchGDBServer(self, host):395 raise self.UnexpectedPacketException()396 397 def qLaunchSuccess(self) -> str:398 return ""399 400 def QEnvironment(self, packet) -> str:401 return "OK"402 403 def QEnvironmentHexEncoded(self, packet) -> str:404 return "OK"405 406 def qRegisterInfo(self, num) -> str:407 return ""408 409 def k(self):410 return ["W01", self.RESPONSE_DISCONNECT]411 412 """413 Raised when we receive a packet for which there is no default action.414 Override the responder class to implement behavior suitable for the test at415 hand.416 """417 418 class UnexpectedPacketException(Exception):419 pass420 421 422class ServerChannel(ABC):423 """424 A wrapper class for TCP or pty-based server.425 """426 427 @abstractmethod428 def get_connect_address(self) -> str:429 """Get address for the client to connect to."""430 431 @abstractmethod432 def get_connect_url(self) -> str:433 """Get URL suitable for process connect command."""434 435 def close_server(self):436 """Close all resources used by the server."""437 438 def accept(self):439 """Accept a single client connection to the server."""440 441 def close_connection(self):442 """Close all resources used by the accepted connection."""443 444 @abstractmethod445 def recv(self) -> bytes:446 """Receive a data packet from the connected client."""447 448 @abstractmethod449 def sendall(self, data: bytes) -> None:450 """Send the data to the connected client."""451 452 453class ServerSocket(ServerChannel):454 def __init__(self, family, type, proto, addr):455 self._server_socket = socket.socket(family, type, proto)456 self._connection = None457 458 self._server_socket.bind(addr)459 self._server_socket.listen(1)460 461 def close_server(self):462 self._server_socket.close()463 464 def accept(self):465 assert self._connection is None466 # accept() is stubborn and won't fail even when the socket is467 # shutdown, so we'll use a timeout468 self._server_socket.settimeout(30.0)469 client, client_addr = self._server_socket.accept()470 # The connected client inherits its timeout from self._socket,471 # but we'll use a blocking socket for the client472 client.settimeout(None)473 self._connection = client474 475 def close_connection(self):476 assert self._connection is not None477 self._connection.close()478 self._connection = None479 480 def recv(self) -> bytes:481 assert self._connection is not None482 return self._connection.recv(4096)483 484 def sendall(self, data: bytes) -> None:485 assert self._connection is not None486 return self._connection.sendall(data)487 488 489class TCPServerSocket(ServerSocket):490 def __init__(self):491 family, type, proto, _, addr = socket.getaddrinfo(492 "localhost", 0, proto=socket.IPPROTO_TCP493 )[0]494 super().__init__(family, type, proto, addr)495 496 def get_connect_address(self) -> str:497 return "[{}]:{}".format(*self._server_socket.getsockname())498 499 def get_connect_url(self) -> str:500 return "connect://" + self.get_connect_address()501 502 503class UnixServerSocket(ServerSocket):504 def __init__(self, addr):505 super().__init__(socket.AF_UNIX, socket.SOCK_STREAM, 0, addr)506 507 def get_connect_address(self) -> str:508 return self._server_socket.getsockname()509 510 def get_connect_url(self) -> str:511 return "unix-connect://" + self.get_connect_address()512 513 514class PtyServerSocket(ServerChannel):515 def __init__(self):516 import pty517 import tty518 519 primary, secondary = pty.openpty()520 tty.setraw(primary)521 self._primary = io.FileIO(primary, "r+b")522 self._secondary = io.FileIO(secondary, "r+b")523 524 def get_connect_address(self) -> str:525 libc = ctypes.CDLL(None)526 libc.ptsname.argtypes = (ctypes.c_int,)527 libc.ptsname.restype = ctypes.c_char_p528 return libc.ptsname(self._primary.fileno()).decode()529 530 def get_connect_url(self):531 return "serial://" + self.get_connect_address()532 533 def close_server(self):534 self._secondary.close()535 self._primary.close()536 537 def recv(self) -> bytes:538 try:539 return self._primary.read(4096)540 except OSError as e:541 # closing the pty results in EIO on Linux, convert it to EOF542 if e.errno == errno.EIO:543 return b""544 raise545 546 def sendall(self, data: bytes) -> None:547 self._primary.write(data)548 549 550class MockGDBServer:551 """552 A simple TCP-based GDB server that can test client behavior by receiving553 commands and issuing custom-tailored responses.554 555 Responses are generated via the .responder property, which should be an556 instance of a class based on MockGDBServerResponder.557 """558 559 responder = None560 _socket = None561 _thread = None562 _receivedData = None563 _receivedDataOffset = None564 _shouldSendAck = True565 566 def __init__(self, socket):567 self._socket = socket568 self.responder = MockGDBServerResponder()569 570 def start(self):571 # Start a thread that waits for a client connection.572 self._thread = threading.Thread(target=self.run)573 self._thread.start()574 575 def stop(self):576 if self._thread is not None:577 self._thread.join()578 self._thread = None579 580 def get_connect_address(self) -> str:581 assert self._socket is not None582 return self._socket.get_connect_address()583 584 def get_connect_url(self) -> str:585 assert self._socket is not None586 return self._socket.get_connect_url()587 588 def run(self):589 assert self._socket is not None590 # For testing purposes, we only need to worry about one client591 # connecting just one time.592 try:593 self._socket.accept()594 except Exception:595 traceback.print_exc()596 return597 self._shouldSendAck = True598 self._receivedData = ""599 self._receivedDataOffset = 0600 data = None601 try:602 while True:603 data = seven.bitcast_to_string(self._socket.recv())604 if data is None or len(data) == 0:605 break606 self._receive(data)607 except self.TerminateConnectionException:608 pass609 except Exception:610 print(611 "An exception happened when receiving the response from the gdb server. Closing the client..."612 )613 traceback.print_exc()614 finally:615 self._socket.close_connection()616 self._socket.close_server()617 618 def _receive(self, data):619 """620 Collects data, parses and responds to as many packets as exist.621 Any leftover data is kept for parsing the next time around.622 """623 self._receivedData += data624 packet = self._parsePacket()625 while packet is not None:626 self._handlePacket(packet)627 packet = self._parsePacket()628 629 def _parsePacket(self):630 """631 Reads bytes from self._receivedData, returning:632 - a packet's contents if a valid packet is found633 - the PACKET_ACK unique object if we got an ack634 - None if we only have a partial packet635 636 Raises an InvalidPacketException if unexpected data is received637 or if checksums fail.638 639 Once a complete packet is found at the front of self._receivedData,640 its data is removed form self._receivedData.641 """642 assert self._receivedData is not None643 data = self._receivedData644 assert self._receivedDataOffset is not None645 i = self._receivedDataOffset646 data_len = len(data)647 if data_len == 0:648 return None649 if i == 0:650 # If we're looking at the start of the received data, that means651 # we're looking for the start of a new packet, denoted by a $.652 # It's also possible we'll see an ACK here, denoted by a +653 if data[0] == "+":654 self._receivedData = data[1:]655 return self.PACKET_ACK656 if ord(data[0]) == 3:657 self._receivedData = data[1:]658 return self.PACKET_INTERRUPT659 if data[0] == "$":660 i += 1661 else:662 raise self.InvalidPacketException(663 "Unexpected leading byte: %s" % data[0]664 )665 666 # If we're looking beyond the start of the received data, then we're667 # looking for the end of the packet content, denoted by a #.668 # Note that we pick up searching from where we left off last time669 while i < data_len and data[i] != "#":670 i += 1671 672 # If there isn't enough data left for a checksum, just remember where673 # we left off so we can pick up there the next time around674 if i > data_len - 3:675 self._receivedDataOffset = i676 return None677 678 # If we have enough data remaining for the checksum, extract it and679 # compare to the packet contents680 packet = data[1:i]681 i += 1682 try:683 check = int(data[i : i + 2], 16)684 except ValueError:685 raise self.InvalidPacketException("Checksum is not valid hex")686 i += 2687 if check != checksum(packet):688 raise self.InvalidPacketException(689 "Checksum %02x does not match content %02x" % (check, checksum(packet))690 )691 # remove parsed bytes from _receivedData and reset offset so parsing692 # can start on the next packet the next time around693 self._receivedData = data[i:]694 self._receivedDataOffset = 0695 return packet696 697 def _sendPacket(self, packet: str):698 assert self._socket is not None699 framed_packet = seven.bitcast_to_bytes(frame_packet(packet))700 self._socket.sendall(framed_packet)701 702 def _handlePacket(self, packet):703 assert self._socket is not None704 if packet is self.PACKET_ACK:705 # Ignore ACKs from the client. For the future, we can consider706 # adding validation code to make sure the client only sends ACKs707 # when it's supposed to.708 return709 response = [""]710 # We'll handle the ack stuff here since it's not something any of the711 # tests will be concerned about, and it'll get turned off quickly anyway.712 if self._shouldSendAck:713 self._socket.sendall(seven.bitcast_to_bytes("+"))714 if packet == "QStartNoAckMode":715 self._shouldSendAck = False716 response = ["OK"]717 elif self.responder is not None:718 # Delegate everything else to our responder719 response = self.responder.respond(packet)720 # MockGDBServerResponder no longer returns non-lists but others like721 # ReverseTestBase still do722 if not isinstance(response, list):723 response = [response]724 for part in response:725 if part is MockGDBServerResponder.RESPONSE_NONE:726 continue727 if part is MockGDBServerResponder.RESPONSE_DISCONNECT:728 raise self.TerminateConnectionException()729 # Should have handled the non-str's above730 assert isinstance(part, str)731 self._sendPacket(part)732 733 PACKET_ACK = object()734 PACKET_INTERRUPT = object()735 736 class TerminateConnectionException(Exception):737 pass738 739 class InvalidPacketException(Exception):740 pass741