#!/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 [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 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 ]