shim/ is an LD_PRELOAD guard around close/fclose/dup2/dup3: - before each close it resolves /proc/self/fd/N; - it reports, with a backtrace, a non-owner close of wallet.dat, database/log.* or blocks/*.dat, and every close that fails with EBADF (the second half of a double close); - FDGUARD_ABORT=1 aborts on the first hit; - run_tests.sh self-tests it (51 checks). repro/ runs a shimmed dragonxd on isolated regtest. Each run gets its own unprivileged network namespace (loopback only). The loads are: - rpc: a hammer with impatient/RST/half-open clients, work-queue bursts and long-polls; - p2p: TLS peers churned by disconnectnode, ban and kill, plus a raw TCP/TLS misbehaver; - tor: a fake control port; - mine: mining with blocknotify and autoshield. tools/tally.py groups the reports by closer. tools/scan_bdb.py finds BDB pages written into block files. With the wolfSSL CloseSocket macro in effect: - p2pmin (300 s) gives one EBADF double close per disconnect; - the full load (1200 s) gives 930 EBADF closes, 186 non-owner closes (37 of wallet/log/blk files) and a 632 KB db.log with BDB0060 PANIC; - RPC-only and Tor-only loads give none. After the fix, every count is 0 and db.log stays empty. The README has the commands and the numbers. Paths come from the environment (GOLDEN, BIN, RUNS, SHIM, ASMAP and the Sapling params). Run output goes to $RUNS, outside the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
4.5 KiB
Python
Executable File
148 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Raw TCP / TLS client churn against the target node's P2P port.
|
|
|
|
dragonxd P2P is TLS 1.3 only (wolfSSL), self-signed, no cert verification. The
|
|
accept path is net.cpp CreateNodeFromAcceptedSocket -> tlsmanager.accept()
|
|
(wolfSSL_accept), and teardown is CloseSocketDisconnect / ~CNode
|
|
(wolfSSL_shutdown + wolfSSL_free + CloseSocket). This driver pokes the accept and
|
|
TLS-teardown/error paths (SSL_read/SSL_write at net.cpp ~1005 and tlsmanager.cpp
|
|
~660-680) by opening many short-lived connections that each misbehave:
|
|
|
|
rst : connect, then abort with RST (SO_LINGER 0) before any TLS bytes
|
|
fin : connect, then close cleanly (FIN) before any TLS bytes
|
|
garbage : connect, send non-ClientHello bytes, then drop
|
|
stall : connect, send a few bytes, hold mid-handshake, drop
|
|
tlsjunk : complete a real TLS 1.3 handshake (verify off), send junk, drop
|
|
tlshalf : start TLS, send a partial ClientHello record, drop
|
|
|
|
Loopback only, against a port this harness started. --duration seconds.
|
|
"""
|
|
import argparse, random, socket, ssl, struct, sys, time, os
|
|
|
|
def mk():
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(3.0)
|
|
return s
|
|
|
|
def rst_close(s):
|
|
try:
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
|
except Exception:
|
|
pass
|
|
try: s.close()
|
|
except Exception: pass
|
|
|
|
def do_rst(host, port):
|
|
s = mk()
|
|
try:
|
|
s.connect((host, port)); rst_close(s)
|
|
except Exception:
|
|
rst_close(s)
|
|
|
|
def do_fin(host, port):
|
|
s = mk()
|
|
try:
|
|
s.connect((host, port)); s.close()
|
|
except Exception:
|
|
try: s.close()
|
|
except Exception: pass
|
|
|
|
def do_garbage(host, port):
|
|
s = mk()
|
|
try:
|
|
s.connect((host, port))
|
|
s.sendall(os.urandom(random.randint(16, 400)))
|
|
time.sleep(random.uniform(0.0, 0.3))
|
|
except Exception:
|
|
pass
|
|
rst_close(s)
|
|
|
|
def do_stall(host, port):
|
|
s = mk()
|
|
try:
|
|
s.connect((host, port))
|
|
# a couple of bytes that look like the start of a TLS record, then wait
|
|
s.sendall(b"\x16\x03\x01")
|
|
time.sleep(random.uniform(0.5, 2.0))
|
|
except Exception:
|
|
pass
|
|
rst_close(s)
|
|
|
|
def _ctx():
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
try:
|
|
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
|
except Exception:
|
|
pass
|
|
return ctx
|
|
|
|
def do_tlsjunk(host, port):
|
|
raw = mk()
|
|
try:
|
|
raw.connect((host, port))
|
|
tls = _ctx().wrap_socket(raw, server_hostname=None)
|
|
# handshake completed; the node now expects a P2P version message.
|
|
# send junk inside the TLS channel, then tear down.
|
|
try:
|
|
tls.sendall(os.urandom(random.randint(8, 128)))
|
|
except Exception:
|
|
pass
|
|
time.sleep(random.uniform(0.0, 0.4))
|
|
try:
|
|
tls.close()
|
|
except Exception:
|
|
rst_close(raw)
|
|
except Exception:
|
|
rst_close(raw)
|
|
|
|
def do_tlshalf(host, port):
|
|
s = mk()
|
|
try:
|
|
s.connect((host, port))
|
|
# a well-formed TLS record header claiming a long handshake, but we send
|
|
# only part of it, then drop -> server sits in wolfSSL_accept WANT_READ.
|
|
body = os.urandom(40)
|
|
rec = b"\x16\x03\x03" + struct.pack(">H", 512) + body
|
|
s.sendall(rec)
|
|
time.sleep(random.uniform(0.3, 1.5))
|
|
except Exception:
|
|
pass
|
|
rst_close(s)
|
|
|
|
HANDLERS = [do_rst, do_fin, do_garbage, do_stall, do_tlsjunk, do_tlshalf]
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--host", default="127.0.0.1")
|
|
ap.add_argument("--port", type=int, required=True)
|
|
ap.add_argument("--duration", type=float, default=300.0)
|
|
ap.add_argument("--concurrency", type=int, default=6)
|
|
ap.add_argument("--rate", type=float, default=0.03, help="sleep between bursts")
|
|
args = ap.parse_args()
|
|
|
|
end = time.time() + args.duration
|
|
counts = {h.__name__: 0 for h in HANDLERS}
|
|
import threading
|
|
lock = threading.Lock()
|
|
|
|
def worker():
|
|
while time.time() < end:
|
|
h = random.choice(HANDLERS)
|
|
h(args.host, args.port)
|
|
with lock:
|
|
counts[h.__name__] += 1
|
|
time.sleep(random.uniform(0.0, args.rate))
|
|
|
|
ts = [threading.Thread(target=worker, daemon=True) for _ in range(args.concurrency)]
|
|
for t in ts: t.start()
|
|
for t in ts: t.join()
|
|
|
|
total = sum(counts.values())
|
|
print(f"tcpchurn: {total} connections in {args.duration:.0f}s -> {counts}", flush=True)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|