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>
98 lines
4.8 KiB
Bash
Executable File
98 lines
4.8 KiB
Bash
Executable File
#!/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 ]
|