DF-2823 / df2823_enolck.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 | /* * DF-2823 -- DragonFlyBSD sys/kern/kern_lockf.c * * BUG: lf_setlock() pre-charges a "worst case" POSIX-lock-count headroom for * the *unlocker's* uid before clipping (kern_lockf.c:510-524). When the uid * is at its RLIMIT_POSIXLOCKS / kern.maxposixlocksperuid limit, a partial * F_UNLCK that clips into the head (or tail) of the caller's own existing * locks FAILS WITH ENOLCK. POSIX (fcntl(2)/POSIX.1-2008 2.9.7) requires * F_UNLCK of one's own locks to succeed; it shall not fail with ENOLCK. * * Reachability on a default system: * - kern.maxposixlocksperuid defaults to maxproc*4 (nonzero => enforced) * - any process may lower its own RLIMIT_POSIXLOCKS (lowering never needs * privilege), so a non-root daemon with a low rlimit exhibits this on a * default install. * * Probe outline: * 1. setrlimit(RLIMIT_POSIXLOCKS, 2) * 2. take lock A=[0,1] and lock B=[2,3] (F_WRLCK) * 3. spam disjoint one-byte locks until the lazily-rolled uid counter * exceeds the limit (per-cpu pups flush into ui_posixlocks past +/-32, * after which further creations are refused with ENOLCK -- expected) * 4. F_UNLCK [1,2]: clips A to [0,0] and B to [3,3]. POSIX: must succeed. * BUG: returns -1/ENOLCK. * 5. full-range F_UNLCK still works (unlock_override path) -- the only * recovery available to a wedged process. * 6. supplementary: negative l_len is rejected with EINVAL (POSIX expects * the range [l_start+l_len, l_start-1] to be accepted). */ #include <sys/types.h> #include <sys/resource.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #ifndef RLIMIT_POSIXLOCKS #define RLIMIT_POSIXLOCKS 11 /* DragonFly */ #endif static int setlk(int fd, int type, off_t start, off_t len) { struct flock fl; memset(&fl, 0, sizeof(fl)); fl.l_type = type; fl.l_whence = SEEK_SET; fl.l_start = start; fl.l_len = len; return (fcntl(fd, F_SETLK, &fl)); } int main(void) { struct rlimit rl; char path[64]; int fd, i, rc, nlocks = 0, refusals = 0; setvbuf(stdout, NULL, _IONBF, 0); snprintf(path, sizeof(path), "/tmp/df2823.%d", getpid()); fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); if (fd < 0) { perror("open"); exit(2); } unlink(path); rl.rlim_cur = 2; rl.rlim_max = 2; if (setrlimit(RLIMIT_POSIXLOCKS, &rl) != 0) { perror("setrlimit(RLIMIT_POSIXLOCKS)"); exit(2); } printf("T1: RLIMIT_POSIXLOCKS lowered to 2 (unprivileged)\n"); if (setlk(fd, F_WRLCK, 0, 2) != 0) { perror("T1: lock A [0,1]"); exit(2); } if (setlk(fd, F_WRLCK, 2, 2) != 0) { perror("T1: lock B [2,3]"); exit(2); } printf("T1: A=[0,1] and B=[2,3] acquired (own locks)\n"); /* * Force the uid-wide counter to roll past the limit. The kernel * keeps per-cpu deltas and only folds them into ui_posixlocks when * a cpu's delta exceeds +/-32, so we need >32 charges. */ for (i = 0; i < 400; ++i) { rc = setlk(fd, F_WRLCK, 10 + (off_t)i * 2, 2); if (rc == 0) { ++nlocks; } else if (errno == ENOLCK) { ++refusals; } else { fprintf(stderr, "T1: filler %d: %s\n", i, strerror(errno)); exit(2); } } printf("T1: filler locks ok=%d refused(ENOLCK)=%d\n", nlocks, refusals); /* * ENOLCK refusals prove the uid-wide counter is at/over the limit * (creation checks the same condition the unlock headroom uses). * Note: A and B plus the fillers must exceed the per-cpu rollup * threshold (32) before ui_posixlocks is charged at all. */ if (refusals == 0 || nlocks < 30) { printf("T1_RESULT: SETUP-FAILED (uid counter never rolled; " "bug state not reached)\n"); exit(3); } /* ---- the actual bug probe: partial unlock clipping A and B ---- */ errno = 0; rc = setlk(fd, F_UNLCK, 1, 2); /* unlock [1,2] */ if (rc == 0) { printf("T1_RESULT: NOT-REPRODUCED (F_UNLCK succeeded)\n"); } else { printf("T1_RESULT: BUG-REPRODUCED F_UNLCK of own locks " "failed rc=%d errno=%d (%s); POSIX requires success\n", rc, errno, strerror(errno)); } /* recovery path: whole-file unlock (unlock_override) */ errno = 0; rc = setlk(fd, F_UNLCK, 0, 0); printf("T1: full-range F_UNLCK rc=%d errno=%d (%s) [expected 0]\n", rc, errno, strerror(errno)); /* supplementary: negative l_len handling */ errno = 0; rc = setlk(fd, F_WRLCK, 10, -5); printf("T3: F_SETLK(start=10,len=-5) rc=%d errno=%d (%s) " "[POSIX expects success: range [5,9]]\n", rc, errno, strerror(errno)); return (0); } |