#!/usr/bin/env python3 """tally.py -- attribute every fdguard report to (type, closer function, target class) and, for EBADF/NON-OWNER reports, to the code that made the previous close of that fd number. The point: show that the stray closes ORIGINATE in CNode code (net.cpp CloseSocketDisconnect / ~CNode closing a number they already closed) and that libevent / BDB / CreateNodeFromAcceptedSocket / GetTransaction only appear as victims-turned-amplifiers downstream of that. """ import collections, re, sys ORIGIN = ("_ZN5CNode21CloseSocketDisconnectEv", "_ZN5CNodeD1Ev", "_ZN5CNodeD2Ev", "_ZN11CNetCleanupD2Ev") NAMES = {"_ZN5CNode21CloseSocketDisconnectEv": "CNode::CloseSocketDisconnect (net.cpp:572)", "_ZN5CNodeD1Ev": "CNode::~CNode (net.cpp:2819)", "_ZN11CNetCleanupD2Ev": "CNetCleanup::~CNetCleanup (net.cpp:2465, shutdown)", "evhttp_connection_free": "libevent evhttp_connection_free (http.c:1244)", "__os_closehandle": "BDB __os_closehandle", "_Z28CreateNodeFromAcceptedSocketjbRK8CAddressS1_": "CreateNodeFromAcceptedSocket (net.cpp:1336)", "_Z14GetTransactionRK7uint256R12CTransactionRS_b": "GetTransaction fclose (main.cpp)", "epoll_nochangelist_add": "libevent epoll ADD", "epoll_nochangelist_del": "libevent epoll DEL/MOD"} def fn(frame_line): m = re.search(r"\+0x[0-9a-f]+ (\S+?)\+0x", frame_line) return m.group(1) if m else "?" def main(path): reps, cur = [], None for line in open(path, errors="replace"): if line.startswith("=== FDGUARD #"): cur = {"hdr": line.strip(), "lines": []} reps.append(cur) elif cur is not None: if line.startswith("=== END"): cur = None else: cur["lines"].append(line.rstrip("\n")) tally = collections.Counter() prev_tally = collections.Counter() db_blk = [] for r in reps: typ = r["hdr"].split()[3] L = r["lines"] cls = next((re.search(r"class=(\S+)", l).group(1) for l in L if " target=" in l and "class=" in l), "?") try: i = next(k for k, l in enumerate(L) if l.strip() == "closer stack:") closer = fn(L[i + 1]) except StopIteration: closer = "?" prev = "-" for k, l in enumerate(L): if "previous successful close of this fd number" in l: # first frame line after the target= line for l2 in L[k + 1:k + 4]: if l2.strip().startswith("#0"): prev = fn(l2) break break tally[(typ, NAMES.get(closer, closer), cls)] += 1 if typ in ("EBADF-CLOSE", "NON-OWNER-CLOSE"): prev_tally[NAMES.get(prev, prev)] += 1 if typ == "NON-OWNER-CLOSE" and cls in ("db", "blk"): tgt = next((l.split("target=")[1].split()[0] for l in L if " target=" in l), "?") db_blk.append((r["hdr"].split()[2], r["hdr"].split()[4], NAMES.get(closer, closer), tgt.rsplit("/", 2)[-2:])) print(f"{len(reps)} reports in {path}") print("\n count type class closer") for (typ, closer, cls), n in tally.most_common(): print(f"{n:6d} {typ:17s} {cls:6s} {closer}") print("\nEBADF + NON-OWNER reports by the code that made the PREVIOUS close of the same number:") for k, n in prev_tally.most_common(): print(f"{n:6d} {k}") o = sum(n for (typ, c, cls), n in tally.items() if typ == "NON-OWNER-CLOSE" and cls in ("db", "blk") and c.startswith(("CNode", "CNetCleanup"))) a = sum(n for (typ, c, cls), n in tally.items() if typ == "NON-OWNER-CLOSE" and cls in ("db", "blk")) - o print(f"\nNON-OWNER closes of wallet/BDB-log/block files: {o + a} (by CNode code directly: {o}; by an amplifier: {a})") for d in db_blk[:40]: print(" ", d[0], d[1], "closer=" + d[2], "target=" + "/".join(d[3])) if __name__ == "__main__": main(sys.argv[1])