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>
207 lines
8.7 KiB
C
207 lines
8.7 KiB
C
/*
|
|
* 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;
|
|
}
|