1
0

dns_test_fixture.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. #!/usr/bin/env python3
  2. """Delayed UDP responder used as a loopback test fixture.
  3. This is a self-contained test fixture for the GetAddrInfoAsyncCancelTest
  4. cases (reproducer for cpp-httplib issue #2431). It is NOT a general-purpose
  5. nameserver and is only intended to run on 127.0.0.1 inside the test job's
  6. own runner / container.
  7. What it does
  8. ------------
  9. Binds a UDP socket on 127.0.0.1:<port>, accepts well-formed DNS queries
  10. from the test process, waits <delay_seconds>, then sends back a minimal
  11. NXDOMAIN reply. The deliberate delay is what makes the bug reproducible:
  12. * The test calls getaddrinfo_with_timeout() with timeout_sec=1.
  13. * gai_suspend() returns EAI_AGAIN after 1s; the function returns and
  14. its stack frame is destroyed.
  15. * The fixture replies after <delay_seconds> (= 3s by default), so the
  16. glibc resolver worker thread receives the response *after* the
  17. caller's frame is gone and writes back into freed stack memory.
  18. * AddressSanitizer (with detect_stack_use_after_return=1) catches the
  19. write and aborts with a stack-use-after-return diagnostic.
  20. Without this fixture the bug is hard to surface: dropping UDP/53 makes
  21. the resolver hang forever, so the worker never receives anything and
  22. never reaches the buggy write-back path.
  23. Usage
  24. -----
  25. python3 test/dns_test_fixture.py <port> [<delay_seconds>]
  26. Only standard library; no third-party dependencies.
  27. """
  28. import socket
  29. import struct
  30. import sys
  31. import threading
  32. import time
  33. def serve(port: int, delay_sec: float) -> None:
  34. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  35. sock.bind(("127.0.0.1", port))
  36. print(
  37. f"[dns_test_fixture] listening on 127.0.0.1:{port}, "
  38. f"reply delay={delay_sec}s",
  39. flush=True,
  40. )
  41. while True:
  42. try:
  43. data, addr = sock.recvfrom(2048)
  44. except OSError:
  45. return
  46. threading.Thread(
  47. target=_reply_after_delay,
  48. args=(sock, data, addr, delay_sec),
  49. daemon=True,
  50. ).start()
  51. def _reply_after_delay(sock, query: bytes, addr, delay_sec: float) -> None:
  52. time.sleep(delay_sec)
  53. if len(query) < 12:
  54. return
  55. # Header: copy transaction id, set QR=1 RA=1 RCODE=3 (NXDOMAIN),
  56. # preserve the requester's RD bit, then echo the question section so
  57. # glibc's resolver accepts the reply as matching its outstanding query.
  58. txid = query[:2]
  59. rd_bit = query[2] & 0x01
  60. flags = struct.pack(">H", 0x8003 | (rd_bit << 8))
  61. counts = struct.pack(">HHHH", 1, 0, 0, 0)
  62. question = query[12:]
  63. reply = txid + flags + counts + question
  64. try:
  65. sock.sendto(reply, addr)
  66. except OSError:
  67. pass
  68. if __name__ == "__main__":
  69. if len(sys.argv) < 2:
  70. print(__doc__, file=sys.stderr)
  71. sys.exit(2)
  72. port_arg = int(sys.argv[1])
  73. delay_arg = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
  74. serve(port_arg, delay_arg)