DragonFlyBSD Kernel Audit
DF-2801 / jailleak.c
← back to finding ↓ download raw
/*
 * DF-2801 — kernel namecache-reference leak on failed jail(2).
 *
 * kern_jail() does cache_copy(&nd.nl_nch, &pr->pr_root) (kern_jail.c:206)
 * which takes a reference on the jail path's namecache entry.  Every error
 * path after that point (kern_jail.c:211-215 assign_prison_id, 224-225
 * prison_sysctl_create, 227-229/234-244 kern_jail_attach failure) cleans
 * up varsymset + nlookup but NEVER calls cache_drop(&pr->pr_root);
 * sys_jail()'s out path then kfrees pr (kern_jail.c:342-351) with the
 * reference still live.
 *
 * Easiest deterministic trigger: jail path = a regular FILE.  sys_jail's
 * nlookup succeeds (it never checks for a directory), cache_copy runs, and
 * kern_jail_attach()->kern_chroot()->checkvp_chdir() fails ENOTDIR
 * (vfs_syscalls.c:2058) -> error path -> leaked reference.  Repeatable
 * forever by root.
 *
 * Measurement: N fresh files; baseline vfs.cache.numcache; N failed
 * jail()s (one per file); unlink all files.  Leaked nch references keep
 * the entries from being reclaimed, so numcache stays ~N above baseline.
 * Control run: same files, but hostname = (char *)1 so sys_jail fails at
 * copyinstr (kern_jail.c:326) BEFORE kern_jail() — no leak expected.
 */
#include <sys/param.h>
#include <sys/jail.h>
#include <sys/sysctl.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>

static long
numcache(void)
{
	int v = 0;
	size_t len = sizeof(v);

	if (sysctlbyname("vfs.cache.numcache", &v, &len, NULL, 0) < 0)
		return (-1);
	return (v);
}

int
main(int argc, char **argv)
{
	int iters = argc > 1 ? atoi(argv[1]) : 2000;
	int control = (argc > 2 && argv[2][0] == 'c');
	char path[128];
	long n0, n1, n2;
	int ok = 0, fail = 0, enotdir = 0;

	if (getuid() != 0) {
		printf("must run as root\n");
		return (2);
	}

	/* create the files first */
	for (int i = 0; i < iters; i++) {
		snprintf(path, sizeof(path), "/tmp/df2801_%d", i);
		int fd = open(path, O_CREAT | O_RDWR, 0644);
		if (fd < 0) {
			printf("setup failed: %s\n", strerror(errno));
			return (2);
		}
		close(fd);
	}
	sleep(1);
	n0 = numcache();
	printf("mode=%s iters=%d\n", control ? "control" : "leak", iters);
	printf("numcache before          : %ld\n", n0);

	for (int i = 0; i < iters; i++) {
		struct jail j;

		memset(&j, 0, sizeof(j));
		j.version = 1;
		snprintf(path, sizeof(path), "/tmp/df2801_%d", i);
		j.path = path;
		j.hostname = control ? (char *)(uintptr_t)0x1 : "leak";
		j.n_ips = 0;
		j.ips = NULL;
		if (syscall(SYS_jail, &j) >= 0) {
			ok++;	/* impossible on purpose */
		} else {
			fail++;
			if (errno == ENOTDIR)
				enotdir++;
		}
	}
	n1 = numcache();
	printf("jail() results           : ok=%d fail=%d (ENOTDIR=%d)\n",
	       ok, fail, enotdir);
	printf("numcache after jail loop : %ld (delta %ld)\n", n1, n1 - n0);

	/* unlink everything and give the system a moment to reclaim */
	for (int i = 0; i < iters; i++) {
		snprintf(path, sizeof(path), "/tmp/df2801_%d", i);
		unlink(path);
	}
	sleep(3);
	n2 = numcache();
	printf("numcache after unlink    : %ld (delta %ld)\n", n2, n2 - n0);

	if (!control && (n2 - n0) > iters / 2) {
		printf("REPRODUCED: ~%ld namecache entries permanently leaked "
		       "(iters=%d)\n", n2 - n0, iters);
		return (1);
	}
	if (control)
		printf("CONTROL: residual delta %ld (expected small)\n", n2 - n0);
	return (0);
}