DF-2801 / jailleak.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | /* * 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); } |