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>
1501 lines
69 KiB
C
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;
|
|
}
|