qa: add fd-guard, the close() guard and regtest harness that found the double close
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>
This commit is contained in:
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Build the fdguard LD_PRELOAD shim and its self-test program.
|
||||
# ./build.sh -> libfdguard.so, fdguard_test
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC=${CC:-gcc}
|
||||
$CC -O2 -g -fPIC -shared -Wall -Wextra -Wno-unused-parameter -fvisibility=default \
|
||||
-o libfdguard.so.tmp fdguard.c -ldl -lpthread
|
||||
# rename, never overwrite in place: a running process may have the old one mapped
|
||||
mv -f libfdguard.so.tmp libfdguard.so
|
||||
# the test program keeps its symbols (no -rdynamic on purpose: like dragonxd,
|
||||
# owner detection must work from .symtab alone) and frame pointers for clarity
|
||||
$CC -O1 -g -fno-omit-frame-pointer -fno-inline -Wall -o fdguard_test.tmp fdguard_test.c -lpthread
|
||||
mv -f fdguard_test.tmp fdguard_test
|
||||
echo "built: $(pwd)/libfdguard.so $(pwd)/fdguard_test"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* fdguard_test.c -- exercises libfdguard.so.
|
||||
*
|
||||
* The function names imitate the real owners inside dragonxd so the shim's
|
||||
* built-in domain patterns apply unchanged:
|
||||
* __os_openhandle / __os_closehandle Berkeley DB os layer (domain bdb)
|
||||
* OpenDiskFile / FlushBlockFile block-file code (domain blk)
|
||||
* ConnectSocketDirectly / CloseSocket P2P sockets (domain p2p)
|
||||
* foreign_stale_close / foreign_* somebody who holds a stale number
|
||||
*
|
||||
* usage: fdguard_test <dir> <case> (see main() for the list)
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NI __attribute__((noinline))
|
||||
static char D[512];
|
||||
static char *P(const char *rel) { static char b[4][700]; static int i; i = (i + 1) & 3; snprintf(b[i], sizeof b[i], "%s/%s", D, rel); return b[i]; }
|
||||
static void say(const char *m) { fprintf(stderr, "[test] %s\n", m); }
|
||||
|
||||
/* --- "Berkeley DB" --- */
|
||||
NI int __os_openhandle(const char *path) { return open(path, O_RDWR | O_CREAT, 0600); }
|
||||
NI int __os_closehandle(int fd) { return close(fd); }
|
||||
/* --- "block files" --- */
|
||||
NI FILE *OpenDiskFile(const char *path) { FILE *f = fopen(path, "rb+"); if (!f) f = fopen(path, "wb+"); return f; }
|
||||
NI int FlushBlockFile(FILE *f) { fflush(f); return fclose(f); }
|
||||
/* --- "P2P" --- */
|
||||
NI int ConnectSocketDirectly(void) { return socket(AF_INET, SOCK_STREAM, 0); }
|
||||
NI int CloseSocket(int *s) { int r = close(*s); if (r) fprintf(stderr, "[test] Socket close failed: %d. Error: %s\n", *s, strerror(errno)); return r; }
|
||||
/* --- foreigners --- */
|
||||
NI int foreign_stale_close(int fd) { return close(fd); }
|
||||
NI int foreign_stale_fclose(FILE *f) { return fclose(f); }
|
||||
NI int foreign_dup2(int fd) { int dn = open("/dev/null", O_RDWR); int r = dup2(dn, fd); close(dn); return r; }
|
||||
NI int foreign_close_range(int fd) { return close_range((unsigned)fd, (unsigned)fd, 0); }
|
||||
NI void foreign_closefrom(int fd) { closefrom(fd); }
|
||||
/* --- plain code that opens and closes in the same function --- */
|
||||
NI int copy_like(const char *src) { int fd = open(src, O_RDONLY); char b[16]; if (fd >= 0) { (void)!read(fd, b, sizeof b); close(fd); } return fd; }
|
||||
NI int plain_open(const char *p) { return open(p, O_RDWR | O_CREAT, 0600); }
|
||||
NI int plain_close(int fd) { return close(fd); }
|
||||
|
||||
static int fd_valid(int fd) { return fcntl(fd, F_GETFD) != -1; }
|
||||
|
||||
static int c_double(void)
|
||||
{
|
||||
int fd = plain_open(P("plain.txt"));
|
||||
plain_close(fd);
|
||||
int r = plain_close(fd); /* EBADF */
|
||||
return r == -1 && errno == EBADF ? 0 : 1;
|
||||
}
|
||||
static int c_dbowner(void) { int fd = __os_openhandle(P("regtest/wallet.dat")); int r = __os_closehandle(fd); return r; }
|
||||
static int c_dbforeign(void)
|
||||
{
|
||||
int fd = __os_openhandle(P("regtest/wallet.dat"));
|
||||
foreign_stale_close(fd); /* HIT */
|
||||
int r = __os_closehandle(fd); /* BDB's own close then fails: EBADF (unless deny) */
|
||||
(void)r;
|
||||
return 0;
|
||||
}
|
||||
static int c_deny(void)
|
||||
{
|
||||
int fd = __os_openhandle(P("regtest/wallet.dat"));
|
||||
foreign_stale_close(fd); /* HIT + DENIED */
|
||||
if (!fd_valid(fd)) { say("deny FAILED: fd closed"); return 1; }
|
||||
if (write(fd, "x", 1) != 1) { say("deny FAILED: write"); return 1; }
|
||||
return __os_closehandle(fd);
|
||||
}
|
||||
static int c_blkowner(void) { FILE *f = OpenDiskFile(P("regtest/blocks/blk00000.dat")); fputs("blk", f); return FlushBlockFile(f); }
|
||||
static int c_blkraw(void)
|
||||
{
|
||||
FILE *f = OpenDiskFile(P("regtest/blocks/blk00001.dat"));
|
||||
foreign_stale_close(fileno(f)); /* HIT: raw close under a live FILE */
|
||||
FlushBlockFile(f); /* EBADF from fclose */
|
||||
return 0;
|
||||
}
|
||||
static int c_stalefile(void)
|
||||
{
|
||||
FILE *f = OpenDiskFile(P("regtest/blocks/blk00002.dat"));
|
||||
int n = fileno(f);
|
||||
foreign_stale_close(n); /* HIT #1 */
|
||||
int w = __os_openhandle(P("regtest/wallet.dat"));
|
||||
if (w != n) fprintf(stderr, "[test] note: wallet got fd %d, FILE had %d\n", w, n);
|
||||
FlushBlockFile(f); /* HIT #2: fclose of a stale FILE whose number is now wallet.dat */
|
||||
if (w == n) __os_closehandle(w); /* EBADF: BDB lost its fd */
|
||||
return 0;
|
||||
}
|
||||
static int c_reuse(void)
|
||||
{
|
||||
int s = ConnectSocketDirectly();
|
||||
int keep = s;
|
||||
CloseSocket(&s); /* legit */
|
||||
int w = __os_openhandle(P("regtest/wallet.dat")); /* reuses the number */
|
||||
if (w != keep) fprintf(stderr, "[test] note: wallet got fd %d, socket was %d\n", w, keep);
|
||||
s = keep;
|
||||
CloseSocket(&s); /* HIT: stale socket number now = wallet.dat */
|
||||
__os_closehandle(w); /* EBADF */
|
||||
return 0;
|
||||
}
|
||||
static int c_dup2(void) { int w = __os_openhandle(P("regtest/wallet.dat")); foreign_dup2(w); __os_closehandle(w); return 0; }
|
||||
static int c_range(void)
|
||||
{
|
||||
int w = __os_openhandle(P("regtest/wallet.dat"));
|
||||
foreign_close_range(w); /* HIT */
|
||||
w = __os_openhandle(P("regtest/database/log.0000000001"));
|
||||
int hi = fcntl(w, F_DUPFD, 600); /* move it high so closefrom only hits it */
|
||||
close(w);
|
||||
foreign_closefrom(hi); /* HIT (domain mismatch: opened by test main via fcntl dup? -> fallback db) */
|
||||
return 0;
|
||||
}
|
||||
static int c_owner_misc(void)
|
||||
{
|
||||
/* nothing here may be flagged */
|
||||
copy_like(P("regtest/wallet.dat")); /* common-caller */
|
||||
int fd = __os_openhandle(P("regtest/blocks/blk00003.dat"));
|
||||
FILE *f = fdopen(fd, "rb+"); /* ownership moves to FILE */
|
||||
FlushBlockFile(f);
|
||||
int s = ConnectSocketDirectly(); CloseSocket(&s);
|
||||
fd = (int)syscall(SYS_openat, AT_FDCWD, P("regtest/wallet.dat"), O_RDWR); /* untracked open */
|
||||
__os_closehandle(fd); /* fallback:bdb */
|
||||
return 0;
|
||||
}
|
||||
static int c_fallback_hit(void)
|
||||
{
|
||||
int fd = (int)syscall(SYS_openat, AT_FDCWD, P("regtest/wallet.dat"), O_RDWR); /* untracked open */
|
||||
foreign_stale_close(fd); /* HIT via fallback rule */
|
||||
return 0;
|
||||
}
|
||||
static int c_epoll(void)
|
||||
{
|
||||
int ep = epoll_create1(0);
|
||||
int fd = plain_open(P("plain2.txt"));
|
||||
struct epoll_event ev = { .events = EPOLLIN };
|
||||
int r = epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev); /* EPERM: regular file */
|
||||
close(fd);
|
||||
r = epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev); /* EBADF */
|
||||
close(ep);
|
||||
return r == -1 ? 0 : 1;
|
||||
}
|
||||
static int c_selfclose(void)
|
||||
{
|
||||
/* close every fd from 3 to 1100 by number, like a buggy daemonizer */
|
||||
for (int i = 3; i < 1100; i++) close(i);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* concurrency + micro-benchmark: open/close churn in many threads */
|
||||
static int nthr = 8, iters = 20000;
|
||||
static void *worker(void *arg)
|
||||
{
|
||||
long id = (long)arg;
|
||||
char p[700]; snprintf(p, sizeof p, "%s/thr%ld.txt", D, id);
|
||||
for (int i = 0; i < iters; i++) {
|
||||
int s = ConnectSocketDirectly(); CloseSocket(&s);
|
||||
int fd = plain_open(p); plain_close(fd);
|
||||
FILE *f = fopen(p, "r"); if (f) fclose(f);
|
||||
if ((i & 255) == 0) { int w = __os_openhandle(P("regtest/wallet.dat")); __os_closehandle(w); }
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
static int c_threads(void)
|
||||
{
|
||||
const char *e = getenv("T_ITERS"); if (e) iters = atoi(e);
|
||||
pthread_t t[64];
|
||||
struct timespec a, b; clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
for (long i = 0; i < nthr; i++) pthread_create(&t[i], NULL, worker, (void *)i);
|
||||
for (int i = 0; i < nthr; i++) pthread_join(t[i], NULL);
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
double ns = (b.tv_sec - a.tv_sec) * 1e9 + (b.tv_nsec - a.tv_nsec);
|
||||
long ops = (long)nthr * iters * 3;
|
||||
printf("threads=%d iters=%d ops=%ld (socket/open/fopen + close each) wall=%.3fs %.0f ns/op-pair/thread\n",
|
||||
nthr, iters, ops, ns / 1e9, ns * nthr / ops);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 3) { fprintf(stderr, "usage: %s <dir> <case>\n", argv[0]); return 2; }
|
||||
snprintf(D, sizeof D, "%s", argv[1]);
|
||||
const char *c = argv[2];
|
||||
if (!strcmp(c, "double")) return c_double();
|
||||
if (!strcmp(c, "dbowner")) return c_dbowner();
|
||||
if (!strcmp(c, "dbforeign")) return c_dbforeign();
|
||||
if (!strcmp(c, "deny")) return c_deny();
|
||||
if (!strcmp(c, "blkowner")) return c_blkowner();
|
||||
if (!strcmp(c, "blkraw")) return c_blkraw();
|
||||
if (!strcmp(c, "stalefile")) return c_stalefile();
|
||||
if (!strcmp(c, "reuse")) return c_reuse();
|
||||
if (!strcmp(c, "dup2")) return c_dup2();
|
||||
if (!strcmp(c, "range")) return c_range();
|
||||
if (!strcmp(c, "ownermisc")) return c_owner_misc();
|
||||
if (!strcmp(c, "fallback")) return c_fallback_hit();
|
||||
if (!strcmp(c, "epoll")) return c_epoll();
|
||||
if (!strcmp(c, "selfclose")) return c_selfclose();
|
||||
if (!strcmp(c, "threads")) return c_threads();
|
||||
fprintf(stderr, "unknown case %s\n", c);
|
||||
return 2;
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Self-test for libfdguard.so. Every case lists the report types it must
|
||||
# produce (exactly, in order) -- an empty list means "no report at all".
|
||||
# ./run_tests.sh (builds first)
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
./build.sh >/dev/null || { echo "build failed"; exit 1; }
|
||||
SO=$PWD/libfdguard.so
|
||||
T=$PWD/t
|
||||
rm -rf "$T"; mkdir -p "$T/regtest/blocks" "$T/regtest/database"
|
||||
pass=0; fail=0
|
||||
|
||||
run() { # run <case> [ENV=VAL ...] -> sets RC, LOG
|
||||
local c=$1; shift
|
||||
LOG=$T/$c${SUFFIX:-}.log; rm -f "$LOG"
|
||||
env FDGUARD_EXE=fdguard_test FDGUARD_LOG="$LOG" "$@" LD_PRELOAD="$SO" ./fdguard_test "$T" "$c" >"$T/$c${SUFFIX:-}.out" 2>&1
|
||||
RC=$?
|
||||
}
|
||||
types() { grep '^=== FDGUARD' "$LOG" 2>/dev/null | awk '{print $4}' | tr '\n' ' ' | sed 's/ $//'; }
|
||||
reasons() { grep -o 'reason=[^ ]*' "$LOG" 2>/dev/null | sed 's/reason=//' | tr '\n' ' ' | sed 's/ $//'; }
|
||||
check() { # check <name> <cond-description> <actual> <expected>
|
||||
if [ "$3" == "$4" ]; then pass=$((pass+1)); printf 'PASS %-12s %s\n' "$1" "$2";
|
||||
else fail=$((fail+1)); printf 'FAIL %-12s %s\n got: [%s]\n want: [%s]\n' "$1" "$2" "$3" "$4"; fi
|
||||
}
|
||||
|
||||
# case | expected report types | expected reasons
|
||||
while IFS='|' read -r c want wantr; do
|
||||
[ -z "$c" ] && continue
|
||||
run "$c"
|
||||
check "$c" "reports" "$(types)" "$want"
|
||||
check "$c" "reasons" "$(reasons)" "$wantr"
|
||||
check "$c" "exit 0" "$RC" "0"
|
||||
done <<'EOF'
|
||||
double|EBADF-CLOSE|
|
||||
dbowner||
|
||||
dbforeign|NON-OWNER-CLOSE EBADF-CLOSE|DOMAIN-MISMATCH
|
||||
blkowner||
|
||||
blkraw|NON-OWNER-CLOSE EBADF-CLOSE|stdio:RAW-CLOSE-UNDER-LIVE-FILE
|
||||
stalefile|NON-OWNER-CLOSE NON-OWNER-CLOSE EBADF-CLOSE|stdio:RAW-CLOSE-UNDER-LIVE-FILE stdio:STALE-FILE
|
||||
reuse|NON-OWNER-CLOSE EBADF-CLOSE|DOMAIN-MISMATCH
|
||||
dup2|NON-OWNER-CLOSE|DOMAIN-MISMATCH
|
||||
range|NON-OWNER-CLOSE NON-OWNER-CLOSE NON-OWNER-CLOSE|DOMAIN-MISMATCH DOMAIN-MISMATCH fallback:DB-FILE-CLOSED-OUTSIDE-BDB
|
||||
ownermisc||
|
||||
fallback|NON-OWNER-CLOSE|fallback:DB-FILE-CLOSED-OUTSIDE-BDB
|
||||
epoll|EPOLL-CTL-EPERM EPOLL-CTL-EBADF|epoll_ctl(ADD)-failed epoll_ctl(ADD)-failed
|
||||
EOF
|
||||
|
||||
# the previous close of the reused number must be the first CloseSocket (proves the double close)
|
||||
run reuse
|
||||
check reuse "history shows 1st CloseSocket on a socket" \
|
||||
"$(grep -A2 'previous successful close' "$LOG" | head -3 | grep -c -e 'class=sock dom=p2p' -e 'CloseSocket+')" "2"
|
||||
# the stale FILE report must name wallet.dat as the victim and OpenDiskFile as the FILE's opener
|
||||
run stalefile
|
||||
check stalefile "victim=wallet.dat, FILE opened in OpenDiskFile" \
|
||||
"$(sed -n '/#2 NON-OWNER/,/=== END #2/p' "$LOG" | grep -m1 -c 'target=.*wallet.dat class=db') $(sed -n '/#2 NON-OWNER/,/=== END #2/p' "$LOG" | grep -A3 'which had been opened by: fopen' | grep -c 'OpenDiskFile+')" "1 1"
|
||||
|
||||
# the log fd survives a close-everything loop and the attempt is reported
|
||||
run selfclose
|
||||
check selfclose "log-fd close refused + reported" "$(grep -c 'CLOSE-OF-FDGUARD-LOG-FD' "$LOG")" "1"
|
||||
check selfclose "log still writable afterwards (final stats)" "$(grep -c '^S .* final' "$LOG")" "1"
|
||||
|
||||
# abort mode: first non-owner close -> SIGABRT (rc 134), report already written
|
||||
SUFFIX=_abort run dbforeign FDGUARD_ABORT=1
|
||||
check abort "rc=134 (SIGABRT)" "$RC" "134"
|
||||
check abort "report written before abort" "$(types)" "NON-OWNER-CLOSE"
|
||||
# abort=2 also aborts on EBADF
|
||||
SUFFIX=_abort2 run double FDGUARD_ABORT=2
|
||||
check abort2 "rc=134 on EBADF" "$RC" "134"
|
||||
|
||||
# deny mode: wallet fd survives the foreign close; no EBADF afterwards
|
||||
SUFFIX=_deny run deny FDGUARD_DENY=1
|
||||
check deny "exit 0 (fd still valid + writable)" "$RC" "0"
|
||||
check deny "reported as DENIED, no EBADF" "$(types) $(grep -c 'ACTION=DENIED' "$LOG")" "NON-OWNER-CLOSE 1"
|
||||
|
||||
# extra classes: guard=all with the owner cases must still be silent
|
||||
SUFFIX=_all run ownermisc FDGUARD_GUARD=all
|
||||
check guard-all "ownermisc silent with FDGUARD_GUARD=all" "$(types)" ""
|
||||
|
||||
# trace-all writes one T line per open/close
|
||||
SUFFIX=_trace run dbowner FDGUARD_TRACE_ALL=1
|
||||
check trace "T lines for open+close" "$(grep -c '^T ' "$LOG")" "2"
|
||||
|
||||
# passive in non-matching processes
|
||||
LOG=$T/passive.log; env FDGUARD_EXE=dragonxd FDGUARD_LOG="$LOG" LD_PRELOAD="$SO" ./fdguard_test "$T" dbforeign >/dev/null 2>&1
|
||||
check passive "no log when exe does not match" "$( [ -e "$LOG" ] && echo exists || echo none)" "none"
|
||||
|
||||
# concurrency: 8 threads x 5000 x (socket, open, fopen) churn, guard=all -> no reports
|
||||
SUFFIX=_thr run threads T_ITERS=5000 FDGUARD_GUARD=all
|
||||
check threads "no reports under 8-thread churn (guard=all)" "$(types)" ""
|
||||
check threads "stats: HITS=0 EBADF=0" "$(grep '^S .*final' "$LOG" | grep -o 'HITS=[0-9]* EBADF=[0-9]*')" "HITS=0 EBADF=0"
|
||||
|
||||
# symbolizer resolves file:line for the test binary
|
||||
check symbolize "addr2line resolved CloseSocket to fdguard_test.c" \
|
||||
"$(./symbolize.py "$T/reuse.log" | grep -m1 -o 'in CloseSocket at .*fdguard_test.c:[0-9]*' | sed 's/.*fdguard_test.c:[0-9]*/ok/')" "ok"
|
||||
|
||||
echo "== $pass passed, $fail failed"
|
||||
[ $fail -eq 0 ]
|
||||
Executable
+212
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
symbolize.py -- turn fdguard log frames into function + file:line.
|
||||
|
||||
Every stack frame in an fdguard report looks like
|
||||
#3 0x5b96b433c991 /path/to/module+0x1991 c_dbforeign+0x2d [bdb]
|
||||
where "+0x1991" is the ELF virtual address inside that module (runtime address
|
||||
minus the module's load bias, so PIE executables and shared libraries need no
|
||||
further adjustment; for a non-PIE executable it equals the absolute address).
|
||||
Frames are return addresses, so we ask addr2line about vaddr-1 (the call
|
||||
instruction), with -f -C -i (function, demangle, inline chain).
|
||||
|
||||
backtrace_symbols_fd lines "module(+0x1991)[0x...]" are symbolized too.
|
||||
|
||||
usage:
|
||||
symbolize.py [options] LOG [LOG...] annotated log on stdout
|
||||
symbolize.py --summary LOG one line per distinct report
|
||||
options:
|
||||
--map OLD=NEW read module OLD from path NEW instead (repeatable),
|
||||
e.g. --map /path/to/deployed/dragonxd=./dragonxd.unstripped
|
||||
--addr2line PATH addr2line binary (default: addr2line)
|
||||
--only-reports drop T/S lines (trace + stats) from the output
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
FRAME_RE = re.compile(r'^(\s*#\d+ 0x[0-9a-f]+ )(\S+)\+0x([0-9a-f]+)(.*)$')
|
||||
BSFD_RE = re.compile(r'^(\S+)\(\+0x([0-9a-f]+)\)\[0x[0-9a-f]+\]\s*$')
|
||||
HDR_RE = re.compile(r'^=== FDGUARD #(\d+) (\S+)')
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
opts = {'map': {}, 'addr2line': 'addr2line', 'summary': False, 'only': False, 'logs': []}
|
||||
it = iter(argv)
|
||||
for a in it:
|
||||
if a == '--map':
|
||||
old, new = next(it).split('=', 1)
|
||||
opts['map'][old] = new
|
||||
elif a.startswith('--map='):
|
||||
old, new = a[6:].split('=', 1)
|
||||
opts['map'][old] = new
|
||||
elif a == '--addr2line':
|
||||
opts['addr2line'] = next(it)
|
||||
elif a == '--summary':
|
||||
opts['summary'] = True
|
||||
elif a == '--only-reports':
|
||||
opts['only'] = True
|
||||
elif a in ('-h', '--help'):
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
else:
|
||||
opts['logs'].append(a)
|
||||
if not opts['logs']:
|
||||
opts['logs'] = ['-']
|
||||
return opts
|
||||
|
||||
|
||||
def read_lines(paths):
|
||||
out = []
|
||||
for p in paths:
|
||||
f = sys.stdin if p == '-' else open(p, errors='replace')
|
||||
out.extend(f.read().splitlines())
|
||||
if f is not sys.stdin:
|
||||
f.close()
|
||||
return out
|
||||
|
||||
|
||||
def resolve_module(mod, opts, known):
|
||||
if mod in opts['map']:
|
||||
return opts['map'][mod]
|
||||
if os.path.exists(mod):
|
||||
return mod
|
||||
base = os.path.basename(mod)
|
||||
for k in known:
|
||||
if os.path.basename(k) == base and os.path.exists(opts['map'].get(k, k)):
|
||||
return opts['map'].get(k, k)
|
||||
return None
|
||||
|
||||
|
||||
def run_addr2line(binary, addrs, a2l):
|
||||
"""addrs: list of int vaddrs (already -1'd). returns {addr: [(func, loc), ...]}"""
|
||||
res = {}
|
||||
if not addrs:
|
||||
return res
|
||||
uniq = sorted(set(addrs))
|
||||
try:
|
||||
p = subprocess.run([a2l, '-a', '-f', '-C', '-i', '-e', binary] + ['0x%x' % a for a in uniq],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False)
|
||||
except FileNotFoundError:
|
||||
return res
|
||||
cur = None
|
||||
lines = p.stdout.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
ln = lines[i]
|
||||
if re.match(r'^0x[0-9a-f]+$', ln):
|
||||
cur = int(ln, 16)
|
||||
res[cur] = []
|
||||
i += 1
|
||||
continue
|
||||
func = ln
|
||||
loc = lines[i + 1] if i + 1 < len(lines) else '??:0'
|
||||
if cur is not None:
|
||||
res[cur].append((func, loc))
|
||||
i += 2
|
||||
return res
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args(sys.argv[1:])
|
||||
lines = read_lines(opts['logs'])
|
||||
|
||||
if opts['summary']:
|
||||
summary(lines)
|
||||
return
|
||||
|
||||
# pass 1: collect modules + addresses
|
||||
want = defaultdict(set)
|
||||
known = OrderedDict()
|
||||
for ln in lines:
|
||||
m = FRAME_RE.match(ln)
|
||||
if m:
|
||||
known[m.group(2)] = 1
|
||||
want[m.group(2)].add(max(int(m.group(3), 16) - 1, 0))
|
||||
for ln in lines:
|
||||
m = BSFD_RE.match(ln)
|
||||
if m:
|
||||
want[m.group(1)].add(max(int(m.group(2), 16) - 1, 0))
|
||||
|
||||
table = {}
|
||||
for mod, addrs in want.items():
|
||||
path = resolve_module(mod, opts, known)
|
||||
if not path:
|
||||
continue
|
||||
table[mod] = run_addr2line(path, list(addrs), opts['addr2line'])
|
||||
|
||||
def fmt(mod, vaddr):
|
||||
r = table.get(mod, {}).get(vaddr)
|
||||
if not r:
|
||||
return []
|
||||
out = []
|
||||
for j, (func, loc) in enumerate(r):
|
||||
if func == '??' and loc.startswith('??'):
|
||||
continue
|
||||
# addr2line -i lists the innermost (inlined) frame first
|
||||
tag = 'inlined:' if j < len(r) - 1 else 'in'
|
||||
out.append('%s %s at %s' % (tag, func, loc))
|
||||
return out
|
||||
|
||||
w = sys.stdout.write
|
||||
for ln in lines:
|
||||
if opts['only'] and (ln.startswith('T ') or ln.startswith('S ')):
|
||||
continue
|
||||
w(ln + '\n')
|
||||
m = FRAME_RE.match(ln)
|
||||
if m:
|
||||
v = max(int(m.group(3), 16) - 1, 0)
|
||||
for s in fmt(m.group(2), v):
|
||||
w(' -> ' + s + '\n')
|
||||
continue
|
||||
m = BSFD_RE.match(ln)
|
||||
if m:
|
||||
v = max(int(m.group(2), 16) - 1, 0)
|
||||
for s in fmt(m.group(1), v):
|
||||
w(' -> ' + s + '\n')
|
||||
|
||||
|
||||
def summary(lines):
|
||||
"""one line per (type, reason, class, first exe closer frame)"""
|
||||
groups = OrderedDict()
|
||||
cur = None
|
||||
in_closer = False
|
||||
for ln in lines:
|
||||
h = HDR_RE.match(ln)
|
||||
if h:
|
||||
cur = {'type': h.group(2), 'reason': '', 'cls': '', 'top': '', 'call': ''}
|
||||
in_closer = False
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
s = ln.strip()
|
||||
if s.startswith('call:'):
|
||||
cur['call'] = s.split('(')[0][6:]
|
||||
elif s.startswith('target=') and ' class=' in s and not cur['cls']:
|
||||
cur['cls'] = s.rsplit('class=', 1)[-1]
|
||||
elif s.startswith('verdict='):
|
||||
cur['reason'] = s.split('reason=', 1)[-1].split(' ')[0]
|
||||
elif s == 'closer stack:':
|
||||
in_closer = True
|
||||
elif in_closer and FRAME_RE.match(ln):
|
||||
parts = s.split(' ')
|
||||
if len(parts) >= 4 and not cur['top'] and '/libc.so' not in parts[2] and '/libstdc++' not in parts[2]:
|
||||
cur['top'] = parts[3]
|
||||
elif in_closer and not s.startswith('#'):
|
||||
in_closer = False
|
||||
if s.startswith('=== END'):
|
||||
k = (cur['type'], cur['call'], cur['reason'], cur['cls'], cur['top'])
|
||||
groups[k] = groups.get(k, 0) + 1
|
||||
cur = None
|
||||
print('%6s %-22s %-12s %-6s %-40s %s' % ('count', 'type', 'call', 'class', 'reason', 'first closer frame'))
|
||||
for (t, c, r, cl, top), n in groups.items():
|
||||
print('%6d %-22s %-12s %-6s %-40s %s' % (n, t[:22], c, cl, r[:40], top))
|
||||
stats = [ln for ln in lines if ln.startswith('S ')]
|
||||
if stats:
|
||||
print('\nlast stats line:\n ' + stats[-1])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user