Files
DanSandClaude Opus 5.5 7131b3673f 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>
2026-10-16 21:23:01 -05:00

1501 lines
69 KiB
C

/*
* fdguard.c -- LD_PRELOAD file-descriptor ownership guard.
*
* Catches code that close()s / fclose()s / dup2()s over a file descriptor
* it does not own (the "stale fd number" bug), and the second half of a
* double close (close() returning EBADF).
*
* How it works
* - close, __close, fclose, dup2, dup3, closefrom, close_range, freopen are
* interposed (dlsym(RTLD_NEXT)). Before the real call the target of
* /proc/self/fd/N is read and classified (db, blk, reg, dev, sock, pipe,
* anon).
* - open/openat/creat/fopen/fdopen/socket/accept/pipe/socketpair/eventfd/
* epoll_create/dup are interposed too, so every fd has an "open record":
* who created it (kind, FILE*, thread, time, stack, "domain").
* - A "domain" is a named set of function-name patterns (bdb = Berkeley DB
* internals, event = libevent, p2p = net/netbase/sock/TLSManager, ...).
* Function names come from the executable's own ELF .symtab (loaded at
* start-up), so this works on a PIE binary built without -rdynamic.
* - Ownership rules for a close of a guarded fd (first match wins):
* stdio fd was created by fopen/fdopen: only fclose/freopen of that
* same FILE* may close it. A raw close() under a live FILE,
* or an fclose of another (stale) FILE, is a non-owner close.
* domain fd created by raw open/socket/accept: the closer's first
* FDGUARD_DEPTH executable frames must share a domain with
* the opener's, or (common-caller) share a function within
* the first FDGUARD_CCDEPTH executable frames (open+close in
* the same function, e.g. boost::filesystem::copy_file).
* fallback no valid open record: db files need a bdb frame, blk
* files need fclose() from a blk-domain frame; other classes
* are "unknown" (counted, never reported as hits).
* - Every non-owner close of a guarded fd is reported with full stacks:
* the closer, the current open record and the previous close of the
* same fd number. Every close()/fclose() that fails with EBADF is
* reported with the previous (successful) close of that number -- that
* is the first half of the double close. epoll_ctl() failing with
* EBADF/EPERM (libevent "Epoll ADD ... failed") is reported the same way.
*
* Hot path: no malloc, no stdio; per-fd records live in a static table;
* output is built in a stack buffer and written with write(2) to a log fd
* that is moved high (>= 900) so the shim does not perturb fd numbering.
* A thread-local flag prevents recursion (and signal-handler re-entry).
*
* Environment (all optional)
* FDGUARD_LOG=path log file ("%p" -> pid). Default /tmp/fdguard.%p.log
* "stderr" = fd 2.
* FDGUARD_EXE=a,b only activate in processes whose exe basename is in
* the list (default "dragonxd"; "*" = every process).
* Children (blocknotify shells) stay passive.
* FDGUARD_GUARD=list classes to judge: db,blk,reg,dev,sock,pipe,anon,all
* (default "db,blk").
* FDGUARD_ABORT=1 abort() after reporting the first non-owner close
* =2 ... or the first EBADF / epoll failure as well.
* FDGUARD_DENY=1 do NOT perform a non-owner close of a db/blk fd
* (return 0). Test aid only.
* FDGUARD_TRACE_ALL=1 one compact line per open/close.
* FDGUARD_HISTORY=0 don't capture stacks for non-guarded opens/closes
* (lower overhead; less context in EBADF reports).
* FDGUARD_DEPTH=n exe frames used for the closer/opener domain (4).
* FDGUARD_CCDEPTH=n exe frames used for the common-caller rule (3).
* FDGUARD_DB_PATHS=a|b substrings classifying a target as db
* (default "/wallet.dat|/database/log.|/__db.").
* FDGUARD_BLK_PATHS=a|b default "/blocks/blk|/blocks/rev".
* FDGUARD_DOMAINS=d=p,p;d2=p extra domains / extra patterns for existing
* ones ("^x" = prefix, otherwise substring).
* FDGUARD_SYMFILE=path read symbols from this ELF instead of
* /proc/self/exe (for a stripped production binary,
* point it at the unstripped copy of the same build).
* FDGUARD_MAXREPORTS=n cap per report type (default 500).
* FDGUARD_STATS_SEC=n periodic stats line every n s (default 60, 0=off).
* FDGUARD_EPOLL=0 don't report epoll_ctl failures.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <elf.h>
#include <errno.h>
#include <execinfo.h>
#include <fcntl.h>
#include <link.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#define FDG_NFD 8192 /* per-fd records kept for fds below this */
#define FDG_NF 20 /* frames stored per open/close record */
#define FDG_NFULL 64 /* frames captured for a report */
#define FDG_TGT 120 /* stored target string length */
#define FDG_MAXDOM 24
#define FDG_MAXPAT 512
#define FDG_LOGFD_MIN 900
#define TLS __thread __attribute__((tls_model("initial-exec")))
/* ------------------------------------------------------------------ kinds */
enum {
K_NONE = 0, K_OPEN, K_FOPEN, K_FDOPEN, K_SOCKET, K_ACCEPT, K_PIPE,
K_SOCKPAIR, K_EVENTFD, K_EPOLL, K_DUP, K_DUPTO,
K_CLOSE, K___CLOSE, K_FCLOSE, K_DUP2, K_DUP3, K_CLOSEFROM, K_CLOSERANGE,
K_FREOPEN, K_MAX
};
static const char *const kname[K_MAX] = {
"none", "open", "fopen", "fdopen", "socket", "accept", "pipe",
"socketpair", "eventfd", "epoll_create", "dup", "dup2/3-target",
"close", "__close", "fclose", "dup2", "dup3", "closefrom", "close_range",
"freopen"
};
enum { C_NONE = 0, C_DB, C_BLK, C_REG, C_DEV, C_SOCK, C_PIPE, C_ANON, C_OTHER, C_MAX };
static const char *const cname[C_MAX] = {
"none", "db", "blk", "reg", "dev", "sock", "pipe", "anon", "other"
};
/* ---------------------------------------------------------------- records */
struct orec {
uint8_t kind, cls;
uint16_t nfr;
uint32_t dom;
int32_t tid;
int32_t ret, err;
uint64_t ts; /* CLOCK_REALTIME ns */
uint64_t dev, ino; /* identity of the open file (fstat) */
void *fp; /* FILE* that owns the fd (stdio) */
char tname[16];
char tgt[FDG_TGT];
void *fr[FDG_NF];
};
struct fdent {
atomic_int lk;
uint32_t gen;
struct orec open; /* current owner (kind==K_NONE: unknown) */
struct orec lclose; /* last successful close of this fd number */
struct orec lclose_open; /* the open record that lclose ended */
};
static struct fdent g_fd[FDG_NFD];
/* ---------------------------------------------------------------- symbols */
struct sym { uintptr_t lo, hi; uint32_t name; uint32_t dom; };
struct domain { char name[16]; int npat; const char *pat[48]; uint32_t nsyms; };
static struct {
int active, inited;
int logfd;
int guard_mask;
int abort_mode, deny, trace_all, history, epoll;
int depth, ccdepth;
int maxreports;
int stats_sec;
char db_paths[512], blk_paths[512];
const char *dbp[16]; int ndbp;
const char *blkp[16]; int nblkp;
char exe_path[512];
uintptr_t exe_bias, exe_lo, exe_hi; /* runtime text range of exe */
uintptr_t shim_lo, shim_hi;
struct sym *syms; size_t nsyms;
char *strs;
struct domain dom[FDG_MAXDOM]; int ndom;
char patbuf[8192]; size_t patlen;
char build_id[64]; /* of the symbol file */
char exe_build_id[64]; /* of the running executable */
int nosyms_reason;
pid_t pid;
} g = { .logfd = -1 };
/* domain bit numbers, fixed for the built-ins (see init_domains) */
enum { D_BDB = 0, D_EVENT, D_BLK, D_LEVELDB, D_BOOSTFS, D_BOOSTIPC, D_RUST,
D_P2P, D_CURL, D_WOLFSSL, D_TOR, D_HTTP, D_DBWRAP, D_NBUILTIN };
/* ---------------------------------------------------------------- counters */
static struct {
atomic_ulong n_close, n_fclose, n_dup2, n_range, n_open, n_epollfail;
atomic_ulong n_guarded, n_owner, n_hits, n_unknown, n_ebadf, n_denied;
atomic_ulong n_closeneg;
atomic_ulong ns_close, ns_fclose, ns_open; /* shim time, excl. real */
atomic_ulong rep_hit, rep_ebadf, rep_epoll;
atomic_ulong seq;
atomic_ulong last_stats;
atomic_ulong rule_cnt[16];
} st;
static const char *const rule_names[16] = {
"stdio:same-FILE", "domain", "common-caller", "fallback:bdb",
"fallback:blk-fclose", "", "", "", "", "", "", "", "", "", "", ""
};
static pthread_mutex_t g_replock = PTHREAD_MUTEX_INITIALIZER;
static TLS int t_in;
static TLS pid_t t_tid;
static TLS char t_tname[16];
static TLS uint64_t t_tname_ns;
/* ---------------------------------------------------------------- reals */
static int (*r_close)(int);
static int (*r___close)(int);
static int (*r_fclose)(FILE *);
static int (*r_dup)(int);
static int (*r_dup2)(int, int);
static int (*r_dup3)(int, int, int);
static void (*r_closefrom)(int);
static int (*r_close_range)(unsigned, unsigned, int);
static int (*r_open)(const char *, int, ...);
static int (*r_open64)(const char *, int, ...);
static int (*r_openat)(int, const char *, int, ...);
static int (*r_openat64)(int, const char *, int, ...);
static int (*r___open_2)(const char *, int);
static int (*r___open64_2)(const char *, int);
static int (*r___openat_2)(int, const char *, int);
static int (*r___openat64_2)(int, const char *, int);
static int (*r_creat)(const char *, mode_t);
static int (*r_creat64)(const char *, mode_t);
static FILE *(*r_fopen)(const char *, const char *);
static FILE *(*r_fopen64)(const char *, const char *);
static FILE *(*r_freopen)(const char *, const char *, FILE *);
static FILE *(*r_freopen64)(const char *, const char *, FILE *);
static FILE *(*r_fdopen)(int, const char *);
static int (*r_socket)(int, int, int);
static int (*r_accept)(int, __SOCKADDR_ARG, socklen_t *);
static int (*r_accept4)(int, __SOCKADDR_ARG, socklen_t *, int);
static int (*r_socketpair)(int, int, int, int *);
static int (*r_pipe)(int *);
static int (*r_pipe2)(int *, int);
static int (*r_eventfd)(unsigned, int);
static int (*r_epoll_create)(int);
static int (*r_epoll_create1)(int);
static int (*r_epoll_ctl)(int, int, int, struct epoll_event *);
#define RESOLVE(n) do { if (!r_##n) r_##n = dlsym(RTLD_NEXT, #n); } while (0)
static void resolve_all(void)
{
RESOLVE(close); RESOLVE(__close); RESOLVE(fclose); RESOLVE(dup);
RESOLVE(dup2); RESOLVE(dup3); RESOLVE(closefrom); RESOLVE(close_range);
RESOLVE(open); RESOLVE(open64); RESOLVE(openat); RESOLVE(openat64);
RESOLVE(__open_2); RESOLVE(__open64_2); RESOLVE(__openat_2);
RESOLVE(__openat64_2); RESOLVE(creat); RESOLVE(creat64);
RESOLVE(fopen); RESOLVE(fopen64); RESOLVE(freopen); RESOLVE(freopen64);
RESOLVE(fdopen); RESOLVE(socket); RESOLVE(accept); RESOLVE(accept4);
RESOLVE(socketpair); RESOLVE(pipe); RESOLVE(pipe2); RESOLVE(eventfd);
RESOLVE(epoll_create); RESOLVE(epoll_create1); RESOLVE(epoll_ctl);
}
static int sys_close(int fd) { return (int)syscall(SYS_close, fd); }
/* ---------------------------------------------------------------- time */
static uint64_t mono_ns(void)
{
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
static uint64_t real_ns(void)
{
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
static pid_t gettid_(void) { return (pid_t)syscall(SYS_gettid); }
/* ---------------------------------------------------------------- output */
struct lb { int fd; size_t n; char b[6144]; };
static void lb_flush(struct lb *l)
{
size_t off = 0;
while (off < l->n) {
ssize_t w = write(l->fd, l->b + off, l->n - off);
if (w < 0) { if (errno == EINTR) continue; break; }
off += (size_t)w;
}
l->n = 0;
}
static void lb_putn(struct lb *l, const char *s, size_t n)
{
while (n) {
size_t room = sizeof(l->b) - l->n;
if (room == 0) { lb_flush(l); room = sizeof(l->b); }
size_t k = n < room ? n : room;
memcpy(l->b + l->n, s, k); l->n += k; s += k; n -= k;
}
}
static void lb_s(struct lb *l, const char *s) { if (!s) s = "(null)"; lb_putn(l, s, strlen(s)); }
static void lb_c(struct lb *l, char c) { lb_putn(l, &c, 1); }
static void lb_u(struct lb *l, unsigned long long v)
{
char t[24]; int i = 24;
do { t[--i] = (char)('0' + v % 10); v /= 10; } while (v);
lb_putn(l, t + i, (size_t)(24 - i));
}
static void lb_d(struct lb *l, long long v) { if (v < 0) { lb_c(l, '-'); lb_u(l, (unsigned long long)(-v)); } else lb_u(l, (unsigned long long)v); }
static void lb_x(struct lb *l, unsigned long long v)
{
static const char hx[] = "0123456789abcdef";
char t[20]; int i = 20;
do { t[--i] = hx[v & 15]; v >>= 4; } while (v);
lb_putn(l, "0x", 2); lb_putn(l, t + i, (size_t)(20 - i));
}
static void lb_u0(struct lb *l, unsigned v, int w)
{
char t[12]; int i = 12;
do { t[--i] = (char)('0' + v % 10); v /= 10; } while (v);
while (12 - i < w) t[--i] = '0';
lb_putn(l, t + i, (size_t)(12 - i));
}
/* UTC ISO-8601 without libc time functions (no locks, no TZ) */
static void lb_ts(struct lb *l, uint64_t ns)
{
if (!ns) { lb_s(l, "-"); return; }
int64_t s = (int64_t)(ns / 1000000000ull);
unsigned us = (unsigned)((ns % 1000000000ull) / 1000);
int64_t days = s / 86400, rem = s % 86400;
int64_t z = days + 719468, era = (z >= 0 ? z : z - 146096) / 146097;
unsigned doe = (unsigned)(z - era * 146097);
unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
int64_t y = (int64_t)yoe + era * 400;
unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
unsigned mp = (5 * doy + 2) / 153, d = doy - (153 * mp + 2) / 5 + 1;
unsigned m = mp < 10 ? mp + 3 : mp - 9;
if (m <= 2) y++;
lb_u0(l, (unsigned)y, 4); lb_c(l, '-'); lb_u0(l, m, 2); lb_c(l, '-'); lb_u0(l, d, 2);
lb_c(l, 'T'); lb_u0(l, (unsigned)(rem / 3600), 2); lb_c(l, ':');
lb_u0(l, (unsigned)(rem / 60 % 60), 2); lb_c(l, ':'); lb_u0(l, (unsigned)(rem % 60), 2);
lb_c(l, '.'); lb_u0(l, us, 6); lb_c(l, 'Z');
}
static void lb_init(struct lb *l) { l->fd = g.logfd >= 0 ? g.logfd : 2; l->n = 0; }
/* ---------------------------------------------------------------- misc */
static void get_tname_raw(char out[16])
{
memset(out, 0, 16);
if (prctl(PR_GET_NAME, out, 0, 0, 0) != 0) out[0] = '?';
out[15] = 0;
}
/* thread name cached per thread, refreshed at most once a second (threads
* rename themselves early, e.g. RenameThread("hush-httpworker")) */
static void get_tname(char out[16])
{
uint64_t now = mono_ns();
if (!t_tname[0] || now - t_tname_ns > 1000000000ull) { get_tname_raw(t_tname); t_tname_ns = now; }
memcpy(out, t_tname, 16);
}
static pid_t my_tid(void) { if (!t_tid) t_tid = gettid_(); return t_tid; }
static void atfork_child(void) { t_tid = 0; t_tname[0] = 0; }
/* readlink /proc/self/fd/N into buf; returns length or -1 */
static int fd_target(int fd, char *buf, size_t sz)
{
char p[40] = "/proc/self/fd/";
size_t n = 14;
char t[16]; int i = 16;
unsigned v = (unsigned)fd;
do { t[--i] = (char)('0' + v % 10); v /= 10; } while (v);
memcpy(p + n, t + i, (size_t)(16 - i)); n += (size_t)(16 - i); p[n] = 0;
ssize_t r = readlink(p, buf, sz - 1);
if (r < 0) { buf[0] = 0; return -1; }
buf[r] = 0;
return (int)r;
}
static int match_list(const char *s, const char *const *pats, int n)
{
for (int i = 0; i < n; i++) if (pats[i][0] && strstr(s, pats[i])) return 1;
return 0;
}
static int classify(const char *t, int len)
{
if (len < 0) return C_NONE;
if (t[0] == '/') {
if (match_list(t, g.dbp, g.ndbp)) return C_DB;
if (match_list(t, g.blkp, g.nblkp)) return C_BLK;
if (!strncmp(t, "/dev/", 5)) return C_DEV;
return C_REG;
}
if (!strncmp(t, "socket:", 7)) return C_SOCK;
if (!strncmp(t, "pipe:", 5)) return C_PIPE;
if (!strncmp(t, "anon_inode:", 11)) return C_ANON;
return C_OTHER;
}
/*
* Identify what fd currently refers to. fstat() first (cheap); sockets and
* pipes get the kernel's "socket:[ino]"/"pipe:[ino]" name synthesized; for a
* regular file the open record's name is reused when (dev,ino) match, else the
* caller's absolute path hint, else readlink(/proc/self/fd/N) (~4us on WSL2).
* Returns the target length, or -1 if fd is not open.
*/
static int fd_target(int fd, char *buf, size_t sz);
static int fd_ident(int fd, const char *hint, const struct orec *rec, char *buf, size_t sz,
uint64_t *dev, uint64_t *ino, int *cls, int *rec_match)
{
struct stat sb;
*rec_match = 0;
if (fd < 0 || fstat(fd, &sb) != 0) { buf[0] = 0; *cls = C_NONE; *dev = *ino = 0; return -1; }
*dev = (uint64_t)sb.st_dev; *ino = (uint64_t)sb.st_ino;
int fmt = sb.st_mode & S_IFMT;
if (fmt == S_IFSOCK || fmt == S_IFIFO) {
const char *pre = fmt == S_IFSOCK ? "socket:[" : "pipe:[";
size_t n = strlen(pre); memcpy(buf, pre, n);
char t[24]; int i = 24; uint64_t v = *ino;
do { t[--i] = (char)('0' + v % 10); v /= 10; } while (v);
memcpy(buf + n, t + i, (size_t)(24 - i)); n += (size_t)(24 - i);
buf[n++] = ']'; buf[n] = 0;
*cls = fmt == S_IFSOCK ? C_SOCK : C_PIPE;
if (rec && rec->kind != K_NONE && rec->dev == *dev && rec->ino == *ino) *rec_match = 1;
return (int)n;
}
if ((fmt == S_IFREG || fmt == S_IFDIR) && rec && rec->kind != K_NONE && rec->dev == *dev && rec->ino == *ino) {
size_t n = strlen(rec->tgt); memcpy(buf, rec->tgt, n + 1);
*cls = rec->cls; *rec_match = 1;
return (int)n;
}
int n;
if ((fmt == S_IFREG || fmt == S_IFDIR) && hint && hint[0] == '/') {
size_t l = strlen(hint); if (l >= sz) l = sz - 1;
memcpy(buf, hint, l); buf[l] = 0; n = (int)l;
} else {
n = fd_target(fd, buf, sz);
if (n < 0) { *cls = C_NONE; return -1; }
/* anon inodes share one inode: require the name to match as well */
if (rec && rec->kind != K_NONE && rec->dev == *dev && rec->ino == *ino) {
char stt[FDG_TGT];
size_t l = (size_t)n;
if (l < FDG_TGT) { memcpy(stt, buf, l + 1); } else { stt[0] = 0; }
if (stt[0] && !strcmp(stt, rec->tgt)) *rec_match = 1;
}
}
*cls = classify(buf, n);
return n;
}
/* store the tail of a long target so the file name survives */
static void store_tgt(char dst[FDG_TGT], const char *src, int len)
{
if (len < 0) { memcpy(dst, "<closed>", 9); return; }
if (len < FDG_TGT) { memcpy(dst, src, (size_t)len + 1); return; }
dst[0] = '.'; dst[1] = '.'; dst[2] = '.';
memcpy(dst + 3, src + len - (FDG_TGT - 4), FDG_TGT - 4);
dst[FDG_TGT - 1] = 0;
}
/* ---------------------------------------------------------------- symbols */
static int in_exe(uintptr_t a) { return a >= g.exe_lo && a < g.exe_hi; }
static int in_shim(uintptr_t a) { return a >= g.shim_lo && a < g.shim_hi; }
static const struct sym *sym_lookup(uintptr_t a)
{
if (!g.nsyms || !in_exe(a)) return NULL;
uintptr_t v = a - g.exe_bias;
size_t lo = 0, hi = g.nsyms;
while (lo < hi) { size_t m = (lo + hi) / 2; if (g.syms[m].lo <= v) lo = m + 1; else hi = m; }
/* lo = first sym with lo > v; walk back over overlapping entries */
for (size_t i = lo; i-- > 0 && lo - i < 8;) {
if (g.syms[i].lo <= v && v < g.syms[i].hi) return &g.syms[i];
}
return NULL;
}
static const char *sym_name(const struct sym *s) { return g.strs + s->name; }
/* compare function names ignoring a GCC clone suffix (.cold, .part.0, ...) */
static int same_fn(const struct sym *a, const struct sym *b)
{
if (a == b) return 1;
const char *x = sym_name(a), *y = sym_name(b);
while (*x && *x == *y && *x != '.') { x++; y++; }
return (*x == 0 || *x == '.') && (*y == 0 || *y == '.');
}
/* frames are return addresses: look up addr-1 so a call at the very end of a
* function (noreturn callee) is attributed to the caller's function */
static const struct sym *frame_sym(void *f) { return sym_lookup((uintptr_t)f - 1); }
static uint32_t stack_dom(void *const *fr, int n, int depth)
{
uint32_t d = 0;
for (int i = 0, k = 0; i < n && k < depth; i++) {
if (!in_exe((uintptr_t)fr[i])) continue;
const struct sym *s = frame_sym(fr[i]);
if (s) d |= s->dom;
k++;
}
return d;
}
/* "common caller": the immediate executable caller of one side (the function
* that called open() or close()) appears within the first `depth` executable
* frames of the other side -- e.g. open+close inside one function, or a close
* helper called from the function that opened the fd. A shared *ancestor*
* further up (main, a thread loop) deliberately does not count. */
static int common_caller(void *const *a, int na, void *const *b, int nb, int depth, const char **which)
{
const struct sym *sa[16], *sb[16]; int ka = 0, kb = 0;
if (depth > 16) depth = 16;
if (depth < 1) return 0;
for (int i = 0; i < na && ka < depth; i++) if (in_exe((uintptr_t)a[i])) { sa[ka++] = frame_sym(a[i]); }
for (int i = 0; i < nb && kb < depth; i++) if (in_exe((uintptr_t)b[i])) { sb[kb++] = frame_sym(b[i]); }
if (ka && sa[0]) for (int j = 0; j < kb; j++) if (sb[j] && same_fn(sa[0], sb[j])) { *which = sym_name(sa[0]); return 1; }
if (kb && sb[0]) for (int i = 0; i < ka; i++) if (sa[i] && same_fn(sb[0], sa[i])) { *which = sym_name(sb[0]); return 1; }
return 0;
}
static int capture(void **out, int max)
{
void *tmp[FDG_NFULL + 8];
int n = backtrace(tmp, FDG_NFULL + 8);
int i = 0;
while (i < n && in_shim((uintptr_t)tmp[i])) i++;
int k = 0;
for (; i < n && k < max; i++) out[k++] = tmp[i];
return k;
}
static void lb_dom(struct lb *l, uint32_t d)
{
if (!d) { lb_s(l, "-"); return; }
int first = 1;
for (int i = 0; i < g.ndom; i++) if (d & (1u << i)) { if (!first) lb_c(l, ','); lb_s(l, g.dom[i].name); first = 0; }
}
static void lb_frame(struct lb *l, int i, void *f)
{
uintptr_t a = (uintptr_t)f;
lb_s(l, " #"); lb_u(l, (unsigned)i); lb_c(l, ' '); lb_x(l, a); lb_c(l, ' ');
if (in_exe(a)) {
lb_s(l, g.exe_path); lb_c(l, '+'); lb_x(l, a - g.exe_bias);
const struct sym *s = frame_sym(f);
if (s) { lb_c(l, ' '); lb_s(l, sym_name(s)); lb_c(l, '+'); lb_x(l, a - g.exe_bias - s->lo);
if (s->dom) { lb_s(l, " ["); lb_dom(l, s->dom); lb_c(l, ']'); } }
} else {
Dl_info di; struct link_map *lm = NULL;
if (dladdr1(f, &di, (void **)&lm, RTLD_DL_LINKMAP) && di.dli_fname) {
lb_s(l, di.dli_fname[0] ? di.dli_fname : "?"); lb_c(l, '+');
lb_x(l, a - (lm ? lm->l_addr : (uintptr_t)di.dli_fbase));
if (di.dli_sname) { lb_c(l, ' '); lb_s(l, di.dli_sname); lb_c(l, '+'); lb_x(l, a - (uintptr_t)di.dli_saddr); }
} else lb_s(l, "?+0x0");
}
lb_c(l, '\n');
}
static void lb_stack(struct lb *l, void *const *fr, int n)
{
for (int i = 0; i < n; i++) lb_frame(l, i, fr[i]);
if (n == 0) lb_s(l, " (no stack recorded)\n");
}
static void lb_rec(struct lb *l, const char *title, const struct orec *r)
{
lb_s(l, " "); lb_s(l, title);
if (r->kind == K_NONE) { lb_s(l, ": none\n"); return; }
lb_s(l, ": "); lb_s(l, kname[r->kind]); lb_s(l, " by tid="); lb_d(l, r->tid);
lb_s(l, " ("); lb_s(l, r->tname); lb_s(l, ") at "); lb_ts(l, r->ts);
lb_s(l, " class="); lb_s(l, cname[r->cls]); lb_s(l, " dom="); lb_dom(l, r->dom);
if (r->fp) { lb_s(l, " FILE="); lb_x(l, (uintptr_t)r->fp); }
if (r->kind >= K_CLOSE) { lb_s(l, " ret="); lb_d(l, r->ret); if (r->ret < 0) { lb_s(l, " errno="); lb_d(l, r->err); } }
lb_s(l, "\n target="); lb_s(l, r->tgt); lb_c(l, '\n');
lb_stack(l, r->fr, r->nfr);
}
/* ---------------------------------------------------------------- fd table */
static void ent_lock(struct fdent *e) { while (atomic_exchange_explicit(&e->lk, 1, memory_order_acquire)) { /* spin */ } }
static void ent_unlock(struct fdent *e) { atomic_store_explicit(&e->lk, 0, memory_order_release); }
static void fill_rec(struct orec *r, int kind, int cls, const char *tgt, int tlen, void *fp,
void *const *fr, int nfr, uint32_t dom)
{
r->kind = (uint8_t)kind; r->cls = (uint8_t)cls; r->dom = dom; r->fp = fp;
r->tid = my_tid(); r->ts = real_ns(); r->ret = 0; r->err = 0;
get_tname(r->tname);
store_tgt(r->tgt, tgt, tlen);
if (nfr > FDG_NF) nfr = FDG_NF;
r->nfr = (uint16_t)nfr;
if (nfr) memcpy(r->fr, fr, sizeof(void *) * (size_t)nfr);
}
static int want_stack_for_class(int cls) { return g.history || (g.guard_mask & (1 << cls)); }
static void trace_line(const char *what, int kind, int fd, int ret, int err, int cls, const char *tgt,
void *const *fr, int nfr, void *caller)
{
struct lb l; lb_init(&l);
char tn[16]; get_tname(tn);
lb_s(&l, "T "); lb_ts(&l, real_ns()); lb_s(&l, " tid="); lb_d(&l, my_tid());
lb_c(&l, ' '); lb_s(&l, tn); lb_c(&l, ' '); lb_s(&l, what ? what : kname[kind]);
lb_s(&l, " fd="); lb_d(&l, fd); lb_s(&l, " ret="); lb_d(&l, ret);
if (ret < 0) { lb_s(&l, " errno="); lb_d(&l, err); }
lb_s(&l, " cls="); lb_s(&l, cname[cls]); lb_s(&l, " tgt="); lb_s(&l, tgt[0] ? tgt : "-");
/* first exe frame = the code that made the call */
void *by = caller;
for (int i = 0; i < nfr; i++) if (in_exe((uintptr_t)fr[i])) { by = fr[i]; break; }
lb_s(&l, " by=");
const struct sym *s = by ? frame_sym(by) : NULL;
if (s) { lb_s(&l, sym_name(s)); lb_c(&l, '+'); lb_x(&l, (uintptr_t)by - g.exe_bias - s->lo); }
else if (by) {
Dl_info di;
if (dladdr(by, &di) && di.dli_sname) lb_s(&l, di.dli_sname); else lb_x(&l, (uintptr_t)by);
} else lb_s(&l, "?");
lb_c(&l, '\n');
lb_flush(&l);
}
static void record_open_h(int fd, int kind, void *fp, void *caller, const char *hint)
{
if (fd < 0) return;
atomic_fetch_add_explicit(&st.n_open, 1, memory_order_relaxed);
char tgt[512]; uint64_t dev, ino; int cls, rm;
int tl = fd_ident(fd, hint, kind == K_FDOPEN && fd < FDG_NFD ? &g_fd[fd].open : NULL, tgt, sizeof tgt, &dev, &ino, &cls, &rm);
void *fr[FDG_NFULL]; int n = 0;
if (want_stack_for_class(cls) || g.trace_all) n = capture(fr, FDG_NFULL);
uint32_t dom = stack_dom(fr, n, g.depth);
if (fd < FDG_NFD) {
struct fdent *e = &g_fd[fd];
ent_lock(e);
if (kind == K_FDOPEN && e->open.kind != K_NONE && e->open.dev == dev && e->open.ino == ino) {
e->open.fp = fp; /* ownership moves to the FILE; keep the opener's stack */
} else {
fill_rec(&e->open, kind, cls, tgt, tl, fp, fr, n, dom);
e->open.dev = dev; e->open.ino = ino;
e->gen++;
}
ent_unlock(e);
}
if (g.trace_all) trace_line(NULL, kind, fd, fd, 0, cls, tgt, fr, n, caller);
}
static void record_open(int fd, int kind, void *fp, void *caller) { record_open_h(fd, kind, fp, caller, NULL); }
/* ---------------------------------------------------------------- reports */
static int report_allowed(atomic_ulong *ctr)
{
return atomic_fetch_add_explicit(ctr, 1, memory_order_relaxed) < (unsigned long)g.maxreports;
}
static void report_header(struct lb *l, const char *type, unsigned long seq)
{
char tn[16]; get_tname(tn);
lb_s(l, "\n=== FDGUARD #"); lb_u(l, seq); lb_c(l, ' '); lb_s(l, type); lb_c(l, ' ');
lb_ts(l, real_ns()); lb_s(l, " pid="); lb_d(l, getpid()); lb_s(l, " tid="); lb_d(l, my_tid());
lb_s(l, " thread="); lb_s(l, tn); lb_c(l, '\n');
}
static void report_bsfd(struct lb *l, void *const *fr, int n)
{
lb_s(l, " backtrace_symbols_fd:\n"); lb_flush(l);
backtrace_symbols_fd((void *const *)fr, n, l->fd);
}
struct verdict { int owner; int rule; const char *why; const char *extra; };
static void report_close(const char *type, int kind, int fd, FILE *fp, const char *tgt, int cls,
const struct verdict *v, uint32_t cdom, void *const *fr, int n,
const struct orec *op, const struct orec *lc, const struct orec *lco,
int ret, int err)
{
unsigned long seq = atomic_fetch_add(&st.seq, 1) + 1;
struct lb l; lb_init(&l);
pthread_mutex_lock(&g_replock);
report_header(&l, type, seq);
lb_s(&l, " call: "); lb_s(&l, kname[kind]); lb_s(&l, "(fd="); lb_d(&l, fd);
if (fp) { lb_s(&l, ", FILE="); lb_x(&l, (uintptr_t)fp); }
lb_s(&l, ")");
if (ret != 0 || err) { lb_s(&l, " ret="); lb_d(&l, ret); lb_s(&l, " errno="); lb_d(&l, err); lb_s(&l, " ("); { const char *en = strerrorname_np(err); lb_s(&l, en ? en : "?"); } lb_c(&l, ')'); }
lb_s(&l, "\n target="); lb_s(&l, tgt[0] ? tgt : "<not open>"); lb_s(&l, " class="); lb_s(&l, cname[cls]);
lb_s(&l, "\n closer-domains="); lb_dom(&l, cdom);
if (v) {
lb_s(&l, "\n verdict="); lb_s(&l, v->owner > 0 ? "OWNER" : v->owner == 0 ? "NON-OWNER" : "UNKNOWN");
lb_s(&l, " reason="); lb_s(&l, v->why); if (v->extra) { lb_c(&l, ' '); lb_s(&l, v->extra); }
if (g.deny && v->owner == 0 && (cls == C_DB || cls == C_BLK)) lb_s(&l, "\n ACTION=DENIED (close not performed, returned 0)");
}
lb_s(&l, "\n closer stack:\n");
lb_stack(&l, fr, n);
if (op) lb_rec(&l, "current open record of this fd", op);
if (lc) lb_rec(&l, "previous successful close of this fd number", lc);
if (lco && lco->kind != K_NONE) lb_rec(&l, " ...which had been opened by", lco);
report_bsfd(&l, fr, n);
lb_s(&l, "=== END #"); lb_u(&l, seq); lb_c(&l, '\n');
lb_flush(&l);
pthread_mutex_unlock(&g_replock);
}
static void maybe_stats(int force);
/* ---------------------------------------------------------------- judging */
static void judge(int kind, FILE *fp, int cls, const struct orec *op, int op_valid,
void *const *fr, int n, uint32_t cdom, struct verdict *v)
{
v->owner = -1; v->rule = -1; v->why = "no-rule"; v->extra = NULL;
int via_stdio = (kind == K_FCLOSE || kind == K_FREOPEN);
if (op_valid && op->fp) {
if (via_stdio && (void *)fp == op->fp) { v->owner = 1; v->rule = 0; v->why = "stdio:same-FILE"; return; }
v->owner = 0;
v->why = via_stdio ? "stdio:STALE-FILE (this FILE* does not own the fd; another FILE does)"
: "stdio:RAW-CLOSE-UNDER-LIVE-FILE (fd belongs to a FILE*)";
return;
}
if (op_valid && via_stdio) { v->owner = 0; v->why = "stdio:STALE-FILE (fd now belongs to a raw open/socket, not to this FILE*)"; return; }
if (!g.nsyms) { v->owner = -1; v->why = "unknown (no symbols: domain rules disabled)"; return; }
if (op_valid) {
if (op->dom & cdom) { v->owner = 1; v->rule = 1; v->why = "domain"; return; }
if (op->dom) { v->owner = 0; v->why = "DOMAIN-MISMATCH (opener has a domain the closer lacks)"; return; }
const char *w = NULL;
if (common_caller(op->fr, op->nfr, fr, n, g.ccdepth, &w)) { v->owner = 1; v->rule = 2; v->why = "common-caller"; v->extra = w; return; }
if (cls == C_DB || cls == C_BLK) { v->owner = 0; v->why = "NO-COMMON-CALLER (db/blk fd opened outside bdb/stdio; closer is unrelated code)"; return; }
if (cdom) { v->owner = 0; v->why = "FOREIGN-COMPONENT (a known component closes an fd created by other code)"; return; }
v->owner = -1; v->why = "unknown (neither side in a known domain)";
return;
}
/* no valid open record */
if (cls == C_DB) {
if (cdom & (1u << D_BDB)) { v->owner = 1; v->rule = 3; v->why = "fallback:bdb"; return; }
v->owner = 0; v->why = "fallback:DB-FILE-CLOSED-OUTSIDE-BDB (no open record)"; return;
}
if (cls == C_BLK) {
if (via_stdio && (cdom & (1u << D_BLK))) { v->owner = 1; v->rule = 4; v->why = "fallback:blk-fclose"; return; }
v->owner = 0; v->why = via_stdio ? "fallback:BLK-FCLOSE-FROM-NON-BLK-CODE (no open record)"
: "fallback:RAW-CLOSE-OF-BLOCK-FILE (no open record)";
return;
}
v->owner = -1; v->why = "unknown (no open record)";
}
/*
* Common pre-close logic. Returns 1 if the real close must be skipped
* (deny mode). Fills *snap with the open record and history for later use.
*/
struct pre {
char tgt[512]; int tlen, cls;
void *fr[FDG_NFULL]; int n;
uint32_t cdom;
int have_ent;
struct orec op, lc, lco; int op_valid;
unsigned long gen; /* g_fd[fd].gen when the close was judged */
};
static int pre_close(int kind, int fd, FILE *fp, struct pre *p)
{
p->n = 0; p->cdom = 0; p->have_ent = 0; p->op_valid = 0;
p->op.kind = K_NONE;
if (fd >= 0 && fd < FDG_NFD) {
struct fdent *e = &g_fd[fd];
ent_lock(e);
p->op = e->open; p->lc = e->lclose; p->lco = e->lclose_open; p->gen = e->gen;
ent_unlock(e);
p->have_ent = 1;
}
uint64_t dev, ino;
p->tlen = fd_ident(fd, NULL, p->have_ent ? &p->op : NULL, p->tgt, sizeof p->tgt, &dev, &ino, &p->cls, &p->op_valid);
int guarded = (g.guard_mask & (1 << p->cls)) != 0;
/* stack: needed for judging (unless the stdio rule settles it), for
* history, for EBADF reports and for trace lines */
int need = g.history || g.trace_all || p->tlen < 0 ||
(guarded && !(p->op_valid && p->op.fp && (kind == K_FCLOSE || kind == K_FREOPEN) && p->op.fp == (void *)fp));
if (need) { p->n = capture(p->fr, FDG_NFULL); p->cdom = stack_dom(p->fr, p->n, g.depth); }
if (!guarded || p->tlen < 0) return 0;
atomic_fetch_add_explicit(&st.n_guarded, 1, memory_order_relaxed);
struct verdict v;
judge(kind, fp, p->cls, &p->op, p->op_valid, p->fr, p->n, p->cdom, &v);
if (v.owner > 0) {
atomic_fetch_add_explicit(&st.n_owner, 1, memory_order_relaxed);
if (v.rule >= 0) atomic_fetch_add_explicit(&st.rule_cnt[v.rule], 1, memory_order_relaxed);
return 0;
}
if (v.owner < 0) { atomic_fetch_add_explicit(&st.n_unknown, 1, memory_order_relaxed); return 0; }
atomic_fetch_add_explicit(&st.n_hits, 1, memory_order_relaxed);
int deny = g.deny && (p->cls == C_DB || p->cls == C_BLK);
if (report_allowed(&st.rep_hit))
report_close("NON-OWNER-CLOSE", kind, fd, fp, p->tgt, p->cls, &v, p->cdom, p->fr, p->n,
p->have_ent ? &p->op : NULL, p->have_ent ? &p->lc : NULL, p->have_ent ? &p->lco : NULL, 0, 0);
if (g.abort_mode >= 1) { maybe_stats(1); abort(); }
if (deny) { atomic_fetch_add_explicit(&st.n_denied, 1, memory_order_relaxed); return 1; }
return 0;
}
static void post_close(int kind, int fd, FILE *fp, struct pre *p, int ret, int err)
{
(void)fp;
if (ret == 0 || (ret < 0 && err != EBADF)) {
/* success (or EINTR/EIO: on Linux the fd is released anyway) */
if (fd >= 0 && fd < FDG_NFD && p->tlen >= 0) {
struct fdent *e = &g_fd[fd];
ent_lock(e);
/* What this close closed is the snapshot taken before it, not g_fd[fd].open now: once the real close
* returned, another thread may already have been handed the same number and recorded its open. Only
* forget the open record if nobody did (same generation), or that thread's later close of its own
* file would be judged with no open record. */
if (p->op_valid) e->lclose_open = p->op; else e->lclose_open.kind = K_NONE;
fill_rec(&e->lclose, kind, p->cls, p->tgt, p->tlen, fp, p->fr, p->n, p->cdom);
e->lclose.dev = p->op_valid ? p->op.dev : 0; e->lclose.ino = p->op_valid ? p->op.ino : 0;
e->lclose.ret = ret; e->lclose.err = ret ? err : 0;
if (!p->have_ent || e->gen == p->gen) {
e->open.kind = K_NONE; e->open.fp = NULL;
e->gen++;
}
ent_unlock(e);
}
} else if (ret < 0 && err == EBADF) {
atomic_fetch_add_explicit(&st.n_ebadf, 1, memory_order_relaxed);
if (p->n == 0) { p->n = capture(p->fr, FDG_NFULL); p->cdom = stack_dom(p->fr, p->n, g.depth); }
if (report_allowed(&st.rep_ebadf)) {
struct orec lc, lco; int have = 0;
if (fd >= 0 && fd < FDG_NFD) { struct fdent *e = &g_fd[fd]; ent_lock(e); lc = e->lclose; lco = e->lclose_open; ent_unlock(e); have = 1; }
report_close("EBADF-CLOSE (second half of a double close?)", kind, fd, fp, p->tgt, p->cls, NULL,
p->cdom, p->fr, p->n, NULL, have ? &lc : NULL, have ? &lco : NULL, ret, err);
}
if (g.abort_mode >= 2) { maybe_stats(1); abort(); }
}
if (g.trace_all) trace_line(NULL, kind, fd, ret, err, p->cls, p->tgt, p->fr, p->n, NULL);
}
/* ---------------------------------------------------------------- stats */
static void maybe_stats(int force)
{
if (g.logfd < 0) return;
uint64_t now = mono_ns();
if (!force) {
if (!g.stats_sec) return;
unsigned long last = atomic_load_explicit(&st.last_stats, memory_order_relaxed);
if (now - last < (uint64_t)g.stats_sec * 1000000000ull) return;
if (!atomic_compare_exchange_strong(&st.last_stats, &last, now)) return;
}
struct lb l; lb_init(&l);
unsigned long nc = st.n_close, nf = st.n_fclose, no = st.n_open;
lb_s(&l, "S "); lb_ts(&l, real_ns()); lb_s(&l, force ? " final" : " periodic");
lb_s(&l, " close="); lb_u(&l, nc); lb_s(&l, " fclose="); lb_u(&l, nf);
lb_s(&l, " dup2/3="); lb_u(&l, st.n_dup2); lb_s(&l, " range="); lb_u(&l, st.n_range);
lb_s(&l, " opens="); lb_u(&l, no); lb_s(&l, " guarded="); lb_u(&l, st.n_guarded);
lb_s(&l, " owner-ok="); lb_u(&l, st.n_owner); lb_s(&l, " unknown="); lb_u(&l, st.n_unknown);
lb_s(&l, " HITS="); lb_u(&l, st.n_hits); lb_s(&l, " EBADF="); lb_u(&l, st.n_ebadf);
lb_s(&l, " epollfail="); lb_u(&l, st.n_epollfail); lb_s(&l, " denied="); lb_u(&l, st.n_denied);
lb_s(&l, " close(-1)="); lb_u(&l, st.n_closeneg);
lb_s(&l, " rules[");
for (int i = 0; i < 5; i++) { if (i) lb_c(&l, ' '); lb_s(&l, rule_names[i]); lb_c(&l, '='); lb_u(&l, st.rule_cnt[i]); }
lb_s(&l, "] shim-ns/call close="); lb_u(&l, nc ? st.ns_close / nc : 0);
lb_s(&l, " fclose="); lb_u(&l, nf ? st.ns_fclose / nf : 0);
lb_s(&l, " open="); lb_u(&l, no ? st.ns_open / no : 0);
lb_s(&l, " shim-total-ms="); lb_u(&l, (st.ns_close + st.ns_fclose + st.ns_open) / 1000000ul);
lb_c(&l, '\n');
lb_flush(&l);
}
/* ---------------------------------------------------------------- close family */
static int guarded_close(int kind, int fd, int (*realfn)(int))
{
if (!realfn) realfn = sys_close;
if (!g.active || t_in) return realfn(fd);
if (fd < 0) { atomic_fetch_add_explicit(&st.n_closeneg, 1, memory_order_relaxed); return realfn(fd); }
if (fd == g.logfd) {
/* someone closes the shim's own (high-numbered) log fd: a stale close */
t_in = 1;
struct pre p;
p.n = capture(p.fr, FDG_NFULL); p.cdom = stack_dom(p.fr, p.n, g.depth);
struct verdict v = { 0, -1, "CLOSE-OF-FDGUARD-LOG-FD (refused)", NULL };
atomic_fetch_add_explicit(&st.n_hits, 1, memory_order_relaxed);
if (report_allowed(&st.rep_hit))
report_close("NON-OWNER-CLOSE", kind, fd, NULL, "fdguard log", C_OTHER, &v, p.cdom, p.fr, p.n, NULL, NULL, NULL, 0, 0);
t_in = 0;
return 0;
}
t_in = 1;
int saved = errno;
uint64_t t0 = mono_ns();
atomic_fetch_add_explicit(&st.n_close, 1, memory_order_relaxed);
struct pre p;
int skip = pre_close(kind, fd, NULL, &p);
uint64_t t1 = mono_ns();
int ret, err;
errno = saved;
if (skip) { ret = 0; err = 0; }
else { ret = realfn(fd); err = ret < 0 ? errno : 0; }
uint64_t t2 = mono_ns();
post_close(kind, fd, NULL, &p, ret, err);
maybe_stats(0);
atomic_fetch_add_explicit(&st.ns_close, (t1 - t0) + (mono_ns() - t2), memory_order_relaxed);
t_in = 0;
errno = ret < 0 ? err : saved;
return ret;
}
int close(int fd) { if (!r_close) RESOLVE(close); return guarded_close(K_CLOSE, fd, r_close); }
int __close(int fd) { if (!r___close) RESOLVE(__close); return guarded_close(K___CLOSE, fd, r___close ? r___close : r_close); }
int fclose(FILE *fp)
{
if (!r_fclose) RESOLVE(fclose);
if (!g.active || t_in || !fp) return r_fclose(fp);
int fd = fileno(fp);
if (fd < 0) return r_fclose(fp);
t_in = 1;
int saved = errno;
uint64_t t0 = mono_ns();
atomic_fetch_add_explicit(&st.n_fclose, 1, memory_order_relaxed);
struct pre p;
int skip = pre_close(K_FCLOSE, fd, fp, &p);
uint64_t t1 = mono_ns();
int ret, err;
errno = 0;
if (skip) {
/* deny: the FILE is stale and its fd number belongs to someone else.
* Leak the FILE (never touch the fd) and report success. */
ret = 0; err = 0;
} else { ret = r_fclose(fp); err = ret != 0 ? errno : 0; }
uint64_t t2 = mono_ns();
post_close(K_FCLOSE, fd, fp, &p, ret == 0 ? 0 : -1, err);
maybe_stats(0);
atomic_fetch_add_explicit(&st.ns_fclose, (t1 - t0) + (mono_ns() - t2), memory_order_relaxed);
t_in = 0;
errno = ret != 0 ? err : saved;
return ret;
}
static int dup_common(int kind, int oldfd, int newfd, int flags)
{
if (kind == K_DUP2) { if (!r_dup2) RESOLVE(dup2); } else { if (!r_dup3) RESOLVE(dup3); }
if (!g.active || t_in || oldfd == newfd || newfd < 0 || newfd == g.logfd) {
if (g.active && !t_in && newfd == g.logfd && newfd >= 0) {
/* would clobber our log: move the log first */
int nl = fcntl(g.logfd, F_DUPFD_CLOEXEC, FDG_LOGFD_MIN + 50);
if (nl >= 0) { int old = g.logfd; g.logfd = nl; (void)old; }
}
return kind == K_DUP2 ? r_dup2(oldfd, newfd) : r_dup3(oldfd, newfd, flags);
}
t_in = 1;
atomic_fetch_add_explicit(&st.n_dup2, 1, memory_order_relaxed);
struct pre p;
int was_open = fcntl(newfd, F_GETFD) != -1;
int skip = 0;
if (was_open) skip = pre_close(kind, newfd, NULL, &p);
int ret, err;
if (skip) { ret = -1; err = EBADF; }
else { ret = kind == K_DUP2 ? r_dup2(oldfd, newfd) : r_dup3(oldfd, newfd, flags); err = ret < 0 ? errno : 0; }
if (ret >= 0) {
if (was_open) post_close(kind, newfd, NULL, &p, 0, 0);
record_open(newfd, K_DUPTO, NULL, __builtin_return_address(0));
}
t_in = 0;
errno = err;
return ret;
}
int dup2(int oldfd, int newfd) { return dup_common(K_DUP2, oldfd, newfd, 0); }
int dup3(int oldfd, int newfd, int flags) { return dup_common(K_DUP3, oldfd, newfd, flags); }
int dup(int oldfd)
{
if (!r_dup) RESOLVE(dup);
int fd = r_dup(oldfd);
if (fd >= 0 && g.active && !t_in) { int e = errno; t_in = 1; record_open(fd, K_DUP, NULL, __builtin_return_address(0)); t_in = 0; errno = e; }
return fd;
}
/* close_range / closefrom: judge every open fd in the range, then do it */
static void range_scan(int kind, unsigned first, unsigned last)
{
int dfd = (int)syscall(SYS_openat, AT_FDCWD, "/proc/self/fd", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dfd < 0) return;
char buf[4096];
for (;;) {
long nr = syscall(SYS_getdents64, dfd, buf, sizeof buf);
if (nr <= 0) break;
for (long off = 0; off < nr;) {
struct { uint64_t ino; int64_t off; unsigned short reclen; unsigned char type; char name[]; } *d = (void *)(buf + off);
off += d->reclen;
if (d->name[0] < '0' || d->name[0] > '9') continue;
unsigned fd = 0; for (const char *c = d->name; *c; c++) fd = fd * 10 + (unsigned)(*c - '0');
if ((int)fd == dfd || fd < first || fd > last || (int)fd == g.logfd) continue;
struct pre p;
(void)pre_close(kind, (int)fd, NULL, &p); /* report only; can't deny inside a range */
post_close(kind, (int)fd, NULL, &p, 0, 0);
}
}
sys_close(dfd);
}
static int do_close_range(unsigned first, unsigned last, int flags)
{
if (!r_close_range) RESOLVE(close_range);
if (r_close_range) return r_close_range(first, last, flags);
long r = syscall(436 /* SYS_close_range */, first, last, flags);
if (r < 0 && errno == ENOSYS && !(flags & 4 /* CLOSE_RANGE_CLOEXEC */)) {
for (unsigned f = first; f <= last && f < 65536; f++) sys_close((int)f);
return 0;
}
return (int)r;
}
/* keep the log fd alive across a range close */
static int range_split(unsigned first, unsigned last, int flags)
{
unsigned lf = g.logfd >= 0 ? (unsigned)g.logfd : ~0u;
if (lf == ~0u || lf < first || lf > last) return do_close_range(first, last, flags);
int r = 0;
if (lf > first) r |= do_close_range(first, lf - 1, flags);
if (lf < last) r |= do_close_range(lf + 1, last, flags);
return r;
}
int close_range(unsigned first, unsigned last, int flags)
{
if (!g.active || t_in) return do_close_range(first, last, flags);
if (flags & 4 /* CLOSE_RANGE_CLOEXEC */) return do_close_range(first, last, flags);
t_in = 1;
atomic_fetch_add_explicit(&st.n_range, 1, memory_order_relaxed);
range_scan(K_CLOSERANGE, first, last);
int r = range_split(first, last, flags);
t_in = 0;
return r;
}
void closefrom(int lowfd)
{
if (lowfd < 0) lowfd = 0;
if (!g.active || t_in) { if (!r_closefrom) RESOLVE(closefrom); if (r_closefrom) r_closefrom(lowfd); else do_close_range((unsigned)lowfd, ~0u, 0); return; }
t_in = 1;
atomic_fetch_add_explicit(&st.n_range, 1, memory_order_relaxed);
range_scan(K_CLOSEFROM, (unsigned)lowfd, ~0u);
range_split((unsigned)lowfd, ~0u, 0);
t_in = 0;
}
FILE *freopen(const char *path, const char *mode, FILE *fp)
{
if (!r_freopen) RESOLVE(freopen);
if (!g.active || t_in || !fp) return r_freopen(path, mode, fp);
int fd = fileno(fp);
t_in = 1;
struct pre p; int had = fd >= 0;
if (had) (void)pre_close(K_FREOPEN, fd, fp, &p);
t_in = 0;
FILE *r = r_freopen(path, mode, fp);
int e = errno;
t_in = 1;
if (had) post_close(K_FREOPEN, fd, fp, &p, 0, 0);
if (r) record_open(fileno(r), K_FOPEN, r, __builtin_return_address(0));
t_in = 0; errno = e;
return r;
}
FILE *freopen64(const char *path, const char *mode, FILE *fp)
{
if (!r_freopen64) RESOLVE(freopen64);
if (!g.active || t_in || !fp) return r_freopen64(path, mode, fp);
int fd = fileno(fp);
t_in = 1;
struct pre p; int had = fd >= 0;
if (had) (void)pre_close(K_FREOPEN, fd, fp, &p);
t_in = 0;
FILE *r = r_freopen64(path, mode, fp);
int e = errno;
t_in = 1;
if (had) post_close(K_FREOPEN, fd, fp, &p, 0, 0);
if (r) record_open(fileno(r), K_FOPEN, r, __builtin_return_address(0));
t_in = 0; errno = e;
return r;
}
/* ---------------------------------------------------------------- open family */
#define OPEN_TAIL(fd, kind, fp) OPEN_TAILH(fd, kind, fp, NULL)
#define OPEN_TAILH(fd, kind, fp, hint) \
do { \
if ((fd) >= 0 && g.active && !t_in) { \
int e_ = errno; t_in = 1; uint64_t t0_ = mono_ns(); \
record_open_h((fd), (kind), (fp), __builtin_return_address(0), (hint)); \
atomic_fetch_add_explicit(&st.ns_open, mono_ns() - t0_, memory_order_relaxed); \
t_in = 0; errno = e_; \
} \
} while (0)
static int needs_mode(int flags) { return (flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE); }
int open(const char *path, int flags, ...)
{
mode_t m = 0; if (needs_mode(flags)) { va_list ap; va_start(ap, flags); m = (mode_t)va_arg(ap, int); va_end(ap); }
if (!r_open) RESOLVE(open);
int fd = r_open(path, flags, m); OPEN_TAILH(fd, K_OPEN, NULL, path); return fd;
}
int open64(const char *path, int flags, ...)
{
mode_t m = 0; if (needs_mode(flags)) { va_list ap; va_start(ap, flags); m = (mode_t)va_arg(ap, int); va_end(ap); }
if (!r_open64) RESOLVE(open64);
int fd = r_open64(path, flags, m); OPEN_TAILH(fd, K_OPEN, NULL, path); return fd;
}
int openat(int dfd, const char *path, int flags, ...)
{
mode_t m = 0; if (needs_mode(flags)) { va_list ap; va_start(ap, flags); m = (mode_t)va_arg(ap, int); va_end(ap); }
if (!r_openat) RESOLVE(openat);
int fd = r_openat(dfd, path, flags, m); OPEN_TAILH(fd, K_OPEN, NULL, path); return fd;
}
int openat64(int dfd, const char *path, int flags, ...)
{
mode_t m = 0; if (needs_mode(flags)) { va_list ap; va_start(ap, flags); m = (mode_t)va_arg(ap, int); va_end(ap); }
if (!r_openat64) RESOLVE(openat64);
int fd = r_openat64(dfd, path, flags, m); OPEN_TAILH(fd, K_OPEN, NULL, path); return fd;
}
int __open_2(const char *path, int flags) { if (!r___open_2) RESOLVE(__open_2); int fd = r___open_2(path, flags); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
int __open64_2(const char *path, int flags) { if (!r___open64_2) RESOLVE(__open64_2); int fd = r___open64_2(path, flags); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
int __openat_2(int d, const char *path, int flags) { if (!r___openat_2) RESOLVE(__openat_2); int fd = r___openat_2(d, path, flags); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
int __openat64_2(int d, const char *path, int flags) { if (!r___openat64_2) RESOLVE(__openat64_2); int fd = r___openat64_2(d, path, flags); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
int creat(const char *path, mode_t m) { if (!r_creat) RESOLVE(creat); int fd = r_creat(path, m); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
int creat64(const char *path, mode_t m) { if (!r_creat64) RESOLVE(creat64); int fd = r_creat64(path, m); OPEN_TAIL(fd, K_OPEN, NULL); return fd; }
FILE *fopen(const char *path, const char *mode)
{
if (!r_fopen) RESOLVE(fopen);
FILE *f = r_fopen(path, mode); if (f) { int fd = fileno(f); OPEN_TAILH(fd, K_FOPEN, f, path); } return f;
}
FILE *fopen64(const char *path, const char *mode)
{
if (!r_fopen64) RESOLVE(fopen64);
FILE *f = r_fopen64(path, mode); if (f) { int fd = fileno(f); OPEN_TAILH(fd, K_FOPEN, f, path); } return f;
}
FILE *fdopen(int fd, const char *mode)
{
if (!r_fdopen) RESOLVE(fdopen);
FILE *f = r_fdopen(fd, mode); if (f) OPEN_TAIL(fd, K_FDOPEN, f); return f;
}
int socket(int d, int t, int p) { if (!r_socket) RESOLVE(socket); int fd = r_socket(d, t, p); OPEN_TAIL(fd, K_SOCKET, NULL); return fd; }
int accept(int s, __SOCKADDR_ARG a, socklen_t *l) { if (!r_accept) RESOLVE(accept); int fd = r_accept(s, a, l); OPEN_TAIL(fd, K_ACCEPT, NULL); return fd; }
int accept4(int s, __SOCKADDR_ARG a, socklen_t *l, int fl) { if (!r_accept4) RESOLVE(accept4); int fd = r_accept4(s, a, l, fl); OPEN_TAIL(fd, K_ACCEPT, NULL); return fd; }
int socketpair(int d, int t, int p, int sv[2])
{
if (!r_socketpair) RESOLVE(socketpair);
int r = r_socketpair(d, t, p, sv); if (r == 0) { OPEN_TAIL(sv[0], K_SOCKPAIR, NULL); OPEN_TAIL(sv[1], K_SOCKPAIR, NULL); } return r;
}
int pipe(int fds[2]) { if (!r_pipe) RESOLVE(pipe); int r = r_pipe(fds); if (r == 0) { OPEN_TAIL(fds[0], K_PIPE, NULL); OPEN_TAIL(fds[1], K_PIPE, NULL); } return r; }
int pipe2(int fds[2], int fl) { if (!r_pipe2) RESOLVE(pipe2); int r = r_pipe2(fds, fl); if (r == 0) { OPEN_TAIL(fds[0], K_PIPE, NULL); OPEN_TAIL(fds[1], K_PIPE, NULL); } return r; }
int eventfd(unsigned v, int fl) { if (!r_eventfd) RESOLVE(eventfd); int fd = r_eventfd(v, fl); OPEN_TAIL(fd, K_EVENTFD, NULL); return fd; }
int epoll_create(int n) { if (!r_epoll_create) RESOLVE(epoll_create); int fd = r_epoll_create(n); OPEN_TAIL(fd, K_EPOLL, NULL); return fd; }
int epoll_create1(int fl) { if (!r_epoll_create1) RESOLVE(epoll_create1); int fd = r_epoll_create1(fl); OPEN_TAIL(fd, K_EPOLL, NULL); return fd; }
/* libevent's "Epoll ADD(..) on fd N failed ... Bad file descriptor /
* Operation not permitted": report with the fd's history */
int epoll_ctl(int epfd, int eop, int fd, struct epoll_event *ev)
{
if (!r_epoll_ctl) RESOLVE(epoll_ctl);
int r = r_epoll_ctl(epfd, eop, fd, ev);
if (r < 0 && g.active && !t_in && g.epoll && (errno == EBADF || errno == EPERM)) {
int err = errno;
t_in = 1;
atomic_fetch_add_explicit(&st.n_epollfail, 1, memory_order_relaxed);
if (report_allowed(&st.rep_epoll)) {
struct pre p;
p.tlen = fd_target(fd, p.tgt, sizeof p.tgt); p.cls = classify(p.tgt, p.tlen); /* rare path: real name */
p.n = capture(p.fr, FDG_NFULL); p.cdom = stack_dom(p.fr, p.n, g.depth);
struct orec op, lc, lco; int have = 0;
if (fd >= 0 && fd < FDG_NFD) { struct fdent *e = &g_fd[fd]; ent_lock(e); op = e->open; lc = e->lclose; lco = e->lclose_open; ent_unlock(e); have = 1; }
char ept[160]; int el = fd_target(epfd, ept, sizeof ept);
char ex[200]; size_t xl = 0;
memcpy(ex, "epfd=", 5); xl = 5;
{ const char *q = el >= 0 ? ept : "<epfd not open>"; size_t ql = strlen(q); if (ql > sizeof ex - xl - 1) ql = sizeof ex - xl - 1; memcpy(ex + xl, q, ql); xl += ql; }
ex[xl] = 0;
struct verdict v = { -1, -1, eop == EPOLL_CTL_ADD ? "epoll_ctl(ADD)-failed" : eop == EPOLL_CTL_MOD ? "epoll_ctl(MOD)-failed" : "epoll_ctl(DEL)-failed", ex };
report_close(err == EPERM ? "EPOLL-CTL-EPERM (fd is not pollable: a regular file?)" : "EPOLL-CTL-EBADF",
K_NONE, fd, NULL, p.tgt, p.cls, &v, p.cdom, p.fr, p.n,
have ? &op : NULL, have ? &lc : NULL, have ? &lco : NULL, r, err);
}
if (g.abort_mode >= 2) { maybe_stats(1); abort(); }
t_in = 0;
errno = err;
}
return r;
}
/* ---------------------------------------------------------------- init */
static int env_int(const char *n, int def) { const char *v = getenv(n); return v && *v ? atoi(v) : def; }
static int split_into(char *buf, const char *src, size_t bufsz, char sep, const char **out, int max)
{
size_t n = strlen(src); if (n >= bufsz) n = bufsz - 1;
memcpy(buf, src, n); buf[n] = 0;
int k = 0; char *s = buf;
while (*s && k < max) {
char *e = strchr(s, sep);
if (e) *e = 0;
if (*s) out[k++] = s;
if (!e) break;
s = e + 1;
}
return k;
}
static const char *pat_store(const char *s, size_t n)
{
if (g.patlen + n + 1 > sizeof g.patbuf) return "";
char *d = g.patbuf + g.patlen; memcpy(d, s, n); d[n] = 0; g.patlen += n + 1; return d;
}
static int dom_find_or_add(const char *name, size_t n)
{
for (int i = 0; i < g.ndom; i++) if (strlen(g.dom[i].name) == n && !strncmp(g.dom[i].name, name, n)) return i;
if (g.ndom >= FDG_MAXDOM) return -1;
struct domain *d = &g.dom[g.ndom];
if (n > 15) n = 15;
memcpy(d->name, name, n); d->name[n] = 0; d->npat = 0;
return g.ndom++;
}
static void dom_add_pats(int di, const char *list) /* comma separated */
{
if (di < 0) return;
struct domain *d = &g.dom[di];
const char *s = list;
while (*s) {
const char *e = strchr(s, ','); size_t n = e ? (size_t)(e - s) : strlen(s);
if (n && d->npat < 48) d->pat[d->npat++] = pat_store(s, n);
if (!e) break;
s = e + 1;
}
}
static void init_domains(void)
{
static const struct { const char *name, *pats; } def[D_NBUILTIN] = {
[D_BDB] = { "bdb", "^__os_,^__db,^__env_,^__memp_,^__log_,^__logc_,^__fop_,^__txn_,^__rep_,^__repmgr_,"
"^__bam,^__ham,^__qam,^__heap,^__blob_,^__lock_,^__mutex_,^__crdel_,^__lv_,^__seq_,"
"^__ram_,^__part,^__crypto_,^__envreg_,^__cdsgroup_,^db_create,^db_env_create,"
"^_ZN2Db,^_ZN5DbEnv,^_ZN3Dbc,^_ZN5DbTxn" },
[D_EVENT] = { "event", "^event_,^evhttp_,^evutil_,^bufferevent_,^be_,^evconnlistener_,^listener_,"
"^evbuffer_,^evmap_,^evsig_,^epoll_,^evthread_,^evdns_,^evrpc_,^evtag_,"
"^evwatch_,^evconn,^accepted_socket_,^server_port_" },
[D_BLK] = { "blk", "CAutoFile,CBufferedFile,FlushBlockFile,FindBlockPos,FindUndoPos,OpenDiskFile,"
"OpenBlockFile,OpenUndoFile,ReadBlockFromDisk,WriteBlockToDisk,UndoWriteToDisk,"
"UndoReadFromDisk,LoadExternalBlockFile,ReadFromDisk,WriteToDisk" },
[D_LEVELDB] = { "leveldb", "7leveldb" },
[D_BOOSTFS] = { "boostfs", "5boost10filesystem" },
[D_BOOSTIPC] = { "boostipc", "5boost12interprocess" },
/* Rust (librustzcash + its crates + std); "$LT$"/".." only occur in Rust legacy-mangled names */
[D_RUST] = { "rust", "^_ZN3std,^_ZN4core,std..,^_ZN5alloc,$LT$,rustzcash,librustzcash_,zcash_,bellman,"
"sapling_crypto,pairing,zip32" },
[D_P2P] = { "p2p", "CloseSocket,ConnectSocket,AcceptConnection,BindListenPort,4hush10TLSManager,"
"ThreadSocketHandler,CreateSock,4Sock,5CNode,Socks5,InterruptibleRecv,StopNode,"
"SetSocketNonBlocking,ThreadOpenConnections,OpenNetworkConnection,ConnectNode" },
[D_CURL] = { "curl", "^Curl_,^curl_" },
[D_WOLFSSL] = { "wolfssl", "^wolfSSL,^wc_,^wolfcrypt" },
[D_TOR] = { "tor", "TorControl,tor_control" },
[D_HTTP] = { "http", "HTTPRequest,HTTPWorkItem,http_request_cb,http_reject_request_cb,HTTPBindAddresses,"
"ThreadHTTP,StartHTTPServer,StopHTTPServer,InterruptHTTPServer,HTTPEvent" },
[D_DBWRAP] = { "dbwrapper", "10CDBWrapper" },
};
for (int i = 0; i < D_NBUILTIN; i++) { int di = dom_find_or_add(def[i].name, strlen(def[i].name)); dom_add_pats(di, def[i].pats); }
const char *x = getenv("FDGUARD_DOMAINS"); /* d=p,p;d2=p */
if (x) {
const char *s = x;
while (*s) {
const char *semi = strchr(s, ';'); size_t n = semi ? (size_t)(semi - s) : strlen(s);
const char *eq = memchr(s, '=', n);
if (eq) {
int di = dom_find_or_add(s, (size_t)(eq - s));
char tmp[1024]; size_t pl = n - (size_t)(eq - s) - 1; if (pl >= sizeof tmp) pl = sizeof tmp - 1;
memcpy(tmp, eq + 1, pl); tmp[pl] = 0;
dom_add_pats(di, tmp);
}
if (!semi) break;
s = semi + 1;
}
}
}
static uint32_t name_dom(const char *nm)
{
uint32_t d = 0;
for (int i = 0; i < g.ndom; i++) {
struct domain *dm = &g.dom[i];
for (int j = 0; j < dm->npat; j++) {
const char *p = dm->pat[j];
if (!p[0]) continue;
if (p[0] == '^' ? !strncmp(nm, p + 1, strlen(p + 1)) : strstr(nm, p) != NULL) { d |= 1u << i; break; }
}
}
return d;
}
static int sym_cmp(const void *a, const void *b)
{
const struct sym *x = a, *y = b;
return x->lo < y->lo ? -1 : x->lo > y->lo ? 1 : (x->hi > y->hi ? -1 : x->hi < y->hi);
}
static void *anon_alloc(size_t n)
{
void *p = mmap(NULL, n ? n : 1, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
return p == MAP_FAILED ? NULL : p;
}
static void load_symbols(const char *path)
{
int fd = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
if (fd < 0) return;
struct stat sb;
if (fstat(fd, &sb) != 0 || sb.st_size < (off_t)sizeof(Elf64_Ehdr)) { sys_close(fd); return; }
unsigned char *m = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
sys_close(fd);
if (m == MAP_FAILED) return;
Elf64_Ehdr *eh = (Elf64_Ehdr *)m;
if (memcmp(eh->e_ident, ELFMAG, SELFMAG) || eh->e_ident[EI_CLASS] != ELFCLASS64 || !eh->e_shoff) goto out;
Elf64_Shdr *sh = (Elf64_Shdr *)(m + eh->e_shoff);
int symi = -1;
for (int i = 0; i < eh->e_shnum; i++) if (sh[i].sh_type == SHT_SYMTAB) { symi = i; break; }
if (symi < 0) for (int i = 0; i < eh->e_shnum; i++) if (sh[i].sh_type == SHT_DYNSYM) { symi = i; break; }
if (symi < 0) goto out;
{
Elf64_Sym *sy = (Elf64_Sym *)(m + sh[symi].sh_offset);
size_t ns = sh[symi].sh_size / sizeof(Elf64_Sym);
const char *str = (const char *)(m + sh[sh[symi].sh_link].sh_offset);
size_t cnt = 0, sbytes = 0;
for (size_t i = 0; i < ns; i++) {
int t = ELF64_ST_TYPE(sy[i].st_info);
if ((t == STT_FUNC || t == STT_GNU_IFUNC) && sy[i].st_size && sy[i].st_shndx != SHN_UNDEF) { cnt++; sbytes += strlen(str + sy[i].st_name) + 1; }
}
g.syms = anon_alloc(cnt * sizeof(struct sym));
g.strs = anon_alloc(sbytes + 1);
if (!g.syms || !g.strs) goto out;
size_t k = 0, so = 0;
for (size_t i = 0; i < ns; i++) {
int t = ELF64_ST_TYPE(sy[i].st_info);
if (!((t == STT_FUNC || t == STT_GNU_IFUNC) && sy[i].st_size && sy[i].st_shndx != SHN_UNDEF)) continue;
const char *nm = str + sy[i].st_name; size_t l = strlen(nm);
memcpy(g.strs + so, nm, l + 1);
g.syms[k].lo = sy[i].st_value; g.syms[k].hi = sy[i].st_value + sy[i].st_size;
g.syms[k].name = (uint32_t)so; g.syms[k].dom = name_dom(nm);
for (int d = 0; d < g.ndom; d++) if (g.syms[k].dom & (1u << d)) g.dom[d].nsyms++;
so += l + 1; k++;
}
qsort(g.syms, k, sizeof(struct sym), sym_cmp);
g.nsyms = k;
}
/* build-id */
for (int i = 0; i < eh->e_shnum; i++) {
if (sh[i].sh_type != SHT_NOTE) continue;
unsigned char *p = m + sh[i].sh_offset, *e = p + sh[i].sh_size;
while (p + 12 <= e) {
uint32_t nsz = *(uint32_t *)p, dsz = *(uint32_t *)(p + 4), typ = *(uint32_t *)(p + 8);
unsigned char *nm = p + 12, *desc = nm + ((nsz + 3) & ~3u);
if (typ == NT_GNU_BUILD_ID && nsz == 4 && !memcmp(nm, "GNU", 4) && dsz <= 31) {
static const char hx[] = "0123456789abcdef";
for (uint32_t j = 0; j < dsz; j++) { g.build_id[2 * j] = hx[desc[j] >> 4]; g.build_id[2 * j + 1] = hx[desc[j] & 15]; }
g.build_id[2 * dsz] = 0;
}
p = desc + ((dsz + 3) & ~3u);
}
}
out:
munmap(m, (size_t)sb.st_size);
}
static int phdr_cb(struct dl_phdr_info *info, size_t sz, void *data)
{
(void)sz;
uintptr_t self_bias = (uintptr_t)data;
int is_exe = (info->dlpi_name == NULL || info->dlpi_name[0] == 0) && !g.exe_hi;
int is_shim = info->dlpi_addr == self_bias && info->dlpi_name && info->dlpi_name[0];
if (!is_exe && !is_shim) return 0;
uintptr_t lo = ~(uintptr_t)0, hi = 0;
for (int i = 0; i < info->dlpi_phnum; i++) {
const ElfW(Phdr) *ph = &info->dlpi_phdr[i];
if (ph->p_type != PT_LOAD) continue;
uintptr_t a = info->dlpi_addr + ph->p_vaddr, b = a + ph->p_memsz;
if (a < lo) lo = a;
if (b > hi) hi = b;
}
if (is_exe) {
g.exe_bias = info->dlpi_addr; g.exe_lo = lo; g.exe_hi = hi;
for (int i = 0; i < info->dlpi_phnum; i++) { /* build-id of the running exe */
const ElfW(Phdr) *ph = &info->dlpi_phdr[i];
if (ph->p_type != PT_NOTE) continue;
unsigned char *p = (unsigned char *)(info->dlpi_addr + ph->p_vaddr), *e = p + ph->p_memsz;
while (p + 12 <= e) {
uint32_t nsz = *(uint32_t *)p, dsz = *(uint32_t *)(p + 4), typ = *(uint32_t *)(p + 8);
unsigned char *nm = p + 12, *desc = nm + ((nsz + 3) & ~3u);
if (typ == NT_GNU_BUILD_ID && nsz == 4 && !memcmp(nm, "GNU", 4) && dsz <= 31) {
static const char hx[] = "0123456789abcdef";
for (uint32_t j = 0; j < dsz; j++) { g.exe_build_id[2 * j] = hx[desc[j] >> 4]; g.exe_build_id[2 * j + 1] = hx[desc[j] & 15]; }
g.exe_build_id[2 * dsz] = 0;
}
p = desc + ((dsz + 3) & ~3u);
}
}
}
else { g.shim_lo = lo; g.shim_hi = hi; }
return 0;
}
static void open_log(void)
{
const char *lp = getenv("FDGUARD_LOG");
if (lp && !strcmp(lp, "stderr")) { g.logfd = fcntl(2, F_DUPFD_CLOEXEC, FDG_LOGFD_MIN); if (g.logfd < 0) g.logfd = 2; return; }
char path[600]; const char *src = lp && *lp ? lp : "/tmp/fdguard.%p.log";
size_t o = 0;
for (const char *s = src; *s && o < sizeof path - 24; s++) {
if (s[0] == '%' && s[1] == 'p') {
char t[16]; int i = 16; unsigned v = (unsigned)getpid();
do { t[--i] = (char)('0' + v % 10); v /= 10; } while (v);
memcpy(path + o, t + i, (size_t)(16 - i)); o += (size_t)(16 - i); s++;
} else path[o++] = *s;
}
path[o] = 0;
int fd = (int)syscall(SYS_openat, AT_FDCWD, path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
if (fd < 0) { g.logfd = -1; return; }
struct rlimit rl; int want = FDG_LOGFD_MIN;
if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY && (rlim_t)want >= rl.rlim_cur) want = (int)rl.rlim_cur - 16;
if (want < 3) want = 3;
int hfd = fcntl(fd, F_DUPFD_CLOEXEC, want);
if (hfd >= 0) { sys_close(fd); fd = hfd; }
g.logfd = fd;
}
static void self_marker(void) {}
__attribute__((constructor)) static void fdg_init(void)
{
t_in = 1;
resolve_all();
g.pid = getpid();
char exe[512]; ssize_t el = readlink("/proc/self/exe", exe, sizeof exe - 1);
if (el < 0) el = 0;
exe[el] = 0;
memcpy(g.exe_path, exe, (size_t)el + 1);
const char *base = strrchr(exe, '/'); base = base ? base + 1 : exe;
const char *want = getenv("FDGUARD_EXE"); if (!want || !*want) want = "dragonxd";
int match = 0;
if (!strcmp(want, "*")) match = 1;
else {
char tmp[512]; const char *lst[32];
int k = split_into(tmp, want, sizeof tmp, ',', lst, 32);
for (int i = 0; i < k; i++) if (!strcmp(lst[i], base)) match = 1;
}
if (!match) { t_in = 0; return; } /* passive in this process */
const char *gv = getenv("FDGUARD_GUARD"); if (!gv || !*gv) gv = "db,blk";
{
char tmp[256]; const char *lst[16];
int k = split_into(tmp, gv, sizeof tmp, ',', lst, 16);
for (int i = 0; i < k; i++) {
if (!strcmp(lst[i], "all")) g.guard_mask |= ~((1 << C_NONE) | (1 << C_OTHER)) & ((1 << C_MAX) - 1);
for (int c = 1; c < C_MAX; c++) if (!strcmp(lst[i], cname[c])) g.guard_mask |= 1 << c;
}
}
g.abort_mode = env_int("FDGUARD_ABORT", 0);
g.deny = env_int("FDGUARD_DENY", 0);
g.trace_all = env_int("FDGUARD_TRACE_ALL", 0);
g.history = env_int("FDGUARD_HISTORY", 1);
g.epoll = env_int("FDGUARD_EPOLL", 1);
g.depth = env_int("FDGUARD_DEPTH", 4);
g.ccdepth = env_int("FDGUARD_CCDEPTH", 3);
g.maxreports = env_int("FDGUARD_MAXREPORTS", 500);
g.stats_sec = env_int("FDGUARD_STATS_SEC", 60);
const char *dbp = getenv("FDGUARD_DB_PATHS"); if (!dbp) dbp = "/wallet.dat|/database/log.|/__db.";
const char *blkp = getenv("FDGUARD_BLK_PATHS"); if (!blkp) blkp = "/blocks/blk|/blocks/rev";
g.ndbp = split_into(g.db_paths, dbp, sizeof g.db_paths, '|', g.dbp, 16);
g.nblkp = split_into(g.blk_paths, blkp, sizeof g.blk_paths, '|', g.blkp, 16);
open_log();
Dl_info di; struct link_map *lm = NULL;
uintptr_t self_bias = 0;
if (dladdr1((void *)&self_marker, &di, (void **)&lm, RTLD_DL_LINKMAP) && lm) self_bias = lm->l_addr;
dl_iterate_phdr(phdr_cb, (void *)self_bias);
init_domains();
const char *sf = getenv("FDGUARD_SYMFILE");
load_symbols(sf && *sf ? sf : "/proc/self/exe");
if (g.nsyms && g.exe_build_id[0] && g.build_id[0] && strcmp(g.exe_build_id, g.build_id)) {
g.nsyms = 0; g.nosyms_reason = 1; /* wrong symbol file: names would be garbage */
}
{ void *w[4]; (void)backtrace(w, 4); } /* warm up: loads libgcc_s now */
atomic_store(&st.last_stats, mono_ns());
struct lb l; lb_init(&l);
lb_s(&l, "# fdguard start "); lb_ts(&l, real_ns()); lb_s(&l, " pid="); lb_d(&l, g.pid);
lb_s(&l, "\n# exe="); lb_s(&l, g.exe_path); lb_s(&l, " build-id="); lb_s(&l, g.build_id[0] ? g.build_id : "?");
lb_s(&l, " bias="); lb_x(&l, g.exe_bias); lb_s(&l, " text=["); lb_x(&l, g.exe_lo); lb_c(&l, ','); lb_x(&l, g.exe_hi);
lb_s(&l, ") syms="); lb_u(&l, g.nsyms); lb_s(&l, " symfile="); lb_s(&l, sf && *sf ? sf : "/proc/self/exe");
lb_s(&l, "\n# guard="); for (int c = 1; c < C_MAX; c++) if (g.guard_mask & (1 << c)) { lb_s(&l, cname[c]); lb_c(&l, ' '); }
lb_s(&l, "abort="); lb_d(&l, g.abort_mode); lb_s(&l, " deny="); lb_d(&l, g.deny); lb_s(&l, " trace_all="); lb_d(&l, g.trace_all);
lb_s(&l, " history="); lb_d(&l, g.history); lb_s(&l, " depth="); lb_d(&l, g.depth); lb_s(&l, " ccdepth="); lb_d(&l, g.ccdepth);
lb_s(&l, " logfd="); lb_d(&l, g.logfd);
lb_s(&l, "\n# db-paths="); lb_s(&l, dbp); lb_s(&l, " blk-paths="); lb_s(&l, blkp);
lb_s(&l, "\n# domains:");
for (int i = 0; i < g.ndom; i++) { lb_c(&l, ' '); lb_s(&l, g.dom[i].name); lb_c(&l, '='); lb_u(&l, g.dom[i].nsyms); }
lb_c(&l, '\n');
lb_flush(&l);
if (g.nosyms_reason == 1) {
lb_s(&l, "# WARNING: FDGUARD_SYMFILE build-id "); lb_s(&l, g.build_id); lb_s(&l, " != running exe build-id ");
lb_s(&l, g.exe_build_id); lb_s(&l, "; symbols ignored\n");
}
if (g.nsyms == 0) {
lb_s(&l, "# WARNING: no function symbols: domain rules disabled (verdict 'unknown'); stdio FILE* ownership,\n"
"# EBADF and epoll reports still work. Point FDGUARD_SYMFILE at an unstripped copy.\n");
}
lb_flush(&l);
pthread_atfork(NULL, NULL, atfork_child);
g.inited = 1;
g.active = 1;
t_in = 0;
}
__attribute__((destructor)) static void fdg_fini(void)
{
if (!g.active) return;
t_in = 1;
maybe_stats(1);
t_in = 0;
}