/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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, "", 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 : ""); 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 : ""; 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; }