1 /* 2 * This file and its contents are supplied under the terms of the 3 * Common Development and Distribution License ("CDDL"), version 1.0. 4 * You may only use this file in accordance with the terms of version 5 * 1.0 of the CDDL. 6 * 7 * A full copy of the text of the CDDL should have accompanied this 8 * source. A copy of the CDDL is also available via the Internet at 9 * http://www.illumos.org/license/CDDL. 10 */ 11 12 /* 13 * Copyright 2026 Oxide Computer Company 14 */ 15 16 /* 17 * Tests for close_range(2): range semantics, CLOEXEC/CLOFORK marking, 18 * argument validation and the cancellation of outstanding asynchronous 19 * I/O against descriptors that are closed. We also exercise 20 * closefrom(3C), which is implemented on top of close_range(). 21 */ 22 23 #include <aio.h> 24 #include <err.h> 25 #include <errno.h> 26 #include <fcntl.h> 27 #include <limits.h> 28 #include <stdbool.h> 29 #include <stdio.h> 30 #include <stdlib.h> 31 #include <string.h> 32 #include <time.h> 33 #include <unistd.h> 34 #include <sys/time.h> 35 36 static uint_t failures = 0; 37 38 #define TFAIL(name, fmt, ...) do { \ 39 (void) fprintf(stderr, "TEST FAILED: %s: " fmt "\n", \ 40 (name), ##__VA_ARGS__); \ 41 failures++; \ 42 } while (0) 43 44 #define TPASS(name) (void) printf("TEST PASSED: %s\n", (name)) 45 46 /* 47 * The test descriptors are pinned at known numbers, comfortably above 48 * anything the process opens for itself. 49 */ 50 #define FD_BASE 50 51 #define FD_LIMIT 70 52 53 static void 54 open_fds(void) 55 { 56 int fd; 57 58 if ((fd = open("/dev/null", O_RDONLY)) == -1) 59 err(EXIT_FAILURE, "could not open /dev/null"); 60 61 for (int i = FD_BASE; i < FD_LIMIT; i++) { 62 if (dup2(fd, i) != i) 63 err(EXIT_FAILURE, "could not dup to fd %d", i); 64 } 65 66 (void) close(fd); 67 } 68 69 /* 70 * Verify that fd is open and carries precisely the descriptor flags in 71 * 'want', or, if want is -1, that fd is closed. 72 */ 73 static bool 74 check_fd(const char *name, int fd, int want) 75 { 76 int fl = fcntl(fd, F_GETFD); 77 78 if (want == -1) { 79 if (fl != -1) { 80 TFAIL(name, "fd %d unexpectedly open (flags %#x)", 81 fd, fl); 82 return (false); 83 } 84 if (errno != EBADF) { 85 TFAIL(name, "fd %d: got errno %s, wanted EBADF", 86 fd, strerrorname_np(errno)); 87 return (false); 88 } 89 return (true); 90 } 91 92 if (fl == -1) { 93 TFAIL(name, "fd %d unexpectedly closed", fd); 94 return (false); 95 } 96 if ((fl & (FD_CLOEXEC | FD_CLOFORK)) != want) { 97 TFAIL(name, "fd %d flags %#x, expected %#x", fd, fl, want); 98 return (false); 99 } 100 return (true); 101 } 102 103 static bool 104 check_range(const char *name, int low, int high, int want) 105 { 106 bool pass = true; 107 108 for (int fd = low; fd <= high; fd++) { 109 if (!check_fd(name, fd, want)) 110 pass = false; 111 } 112 return (pass); 113 } 114 115 static void 116 expect_einval(const char *name, uint_t low, uint_t high, int flags) 117 { 118 int ret = close_range(low, high, flags); 119 120 if (ret != -1) { 121 TFAIL(name, "close_range returned %d, expected failure", ret); 122 return; 123 } 124 if (errno != EINVAL) { 125 TFAIL(name, "got errno %s, wanted EINVAL", 126 strerrorname_np(errno)); 127 return; 128 } 129 TPASS(name); 130 } 131 132 /* 133 * Render an aio_error() result, or the -1 used here to indicate a 134 * timeout, for a failure message. 135 */ 136 static const char * 137 errname(int e) 138 { 139 const char *n; 140 141 if (e == -1) 142 return ("EINPROGRESS (timed out)"); 143 if (e == 0) 144 return ("success"); 145 if ((n = strerrorname_np(e)) == NULL) 146 return ("unknown error"); 147 return (n); 148 } 149 150 /* 151 * Create a pipe with the read end at the requested descriptor, returning 152 * the write end. 153 */ 154 static int 155 pipe_at(int rfd) 156 { 157 int p[2]; 158 159 if (pipe(p) == -1) 160 err(EXIT_FAILURE, "pipe"); 161 if (dup2(p[0], rfd) != rfd) 162 err(EXIT_FAILURE, "could not dup pipe to fd %d", rfd); 163 (void) close(p[0]); 164 return (p[1]); 165 } 166 167 /* 168 * Start an asynchronous read against fd. The pipes used here are kept 169 * empty, so the request stays outstanding until data arrives or it is 170 * cancelled. 171 */ 172 static void 173 start_aio(struct aiocb *cb, int fd, char *buf, size_t bufsz) 174 { 175 (void) memset(cb, 0, sizeof (*cb)); 176 cb->aio_fildes = fd; 177 cb->aio_buf = buf; 178 cb->aio_nbytes = bufsz; 179 if (aio_read(cb) != 0) 180 err(EXIT_FAILURE, "aio_read(fd %d)", fd); 181 } 182 183 /* 184 * Wait for an asynchronous request to leave the in-progress state and 185 * return its final aio_error() value, or -1 if it is still in progress 186 * after five seconds. 187 */ 188 static int 189 await_aio(const struct aiocb *cb) 190 { 191 const struct timespec ts = { .tv_sec = 0, .tv_nsec = MSEC2NSEC(1) }; 192 193 for (int i = 0; i < 5000; i++) { 194 int e = aio_error(cb); 195 196 if (e != EINPROGRESS) 197 return (e); 198 (void) nanosleep(&ts, NULL); 199 } 200 return (-1); 201 } 202 203 /* 204 * Closing a range must cancel outstanding user-level aio requests 205 * against the descriptors within it, and only those. A range that is 206 * only having descriptor flags set must not disturb aio at all. 207 */ 208 static void 209 run_aio_tests(void) 210 { 211 struct aiocb cb_range, cb_below, cb_flag, cb_from; 212 char buf_range[4], buf_below[4], buf_flag[4], buf_from[4]; 213 ssize_t rv; 214 int wfd, e; 215 216 (void) pipe_at(80); 217 wfd = pipe_at(40); 218 start_aio(&cb_range, 80, buf_range, sizeof (buf_range)); 219 start_aio(&cb_below, 40, buf_below, sizeof (buf_below)); 220 221 /* Closing a range cancels requests against descriptors within it */ 222 if (close_range(60, UINT_MAX, 0) != 0) 223 err(EXIT_FAILURE, "close_range(60, UINT_MAX, 0)"); 224 if ((e = await_aio(&cb_range)) != ECANCELED) { 225 TFAIL("aio-cancel-range", "request finished with %s, " 226 "wanted ECANCELED", errname(e)); 227 } else if (aio_return(&cb_range) != -1) { 228 TFAIL("aio-cancel-range", "aio_return did not report an " 229 "error after cancellation"); 230 } else if (check_fd("aio-cancel-range", 80, -1)) { 231 TPASS("aio-cancel-range"); 232 } 233 234 /* Requests against descriptors below the range are untouched */ 235 if (write(wfd, "ok", 2) != 2) 236 err(EXIT_FAILURE, "write to pipe"); 237 if ((e = await_aio(&cb_below)) != 0) { 238 TFAIL("aio-below-range", "request finished with %s, " 239 "wanted success", errname(e)); 240 } else if ((rv = aio_return(&cb_below)) != 2) { 241 TFAIL("aio-below-range", "aio_return gave %zd, wanted 2", rv); 242 } else { 243 TPASS("aio-below-range"); 244 } 245 246 /* Setting descriptor flags must not cancel outstanding aio */ 247 start_aio(&cb_flag, 40, buf_flag, sizeof (buf_flag)); 248 if (close_range(40, 40, CLOSE_RANGE_CLOFORK) != 0) 249 err(EXIT_FAILURE, "close_range(40, 40, CLOSE_RANGE_CLOFORK)"); 250 if (close_range(40, 40, CLOSE_RANGE_CLOEXEC) != 0) 251 err(EXIT_FAILURE, "close_range(40, 40, CLOSE_RANGE_CLOEXEC)"); 252 e = aio_error(&cb_flag); 253 if (write(wfd, "ok", 2) != 2) 254 err(EXIT_FAILURE, "write to pipe"); 255 if (e != EINPROGRESS) { 256 TFAIL("aio-mark-keeps", "request no longer in progress " 257 "after flag marking: %s", errname(e)); 258 } else if ((e = await_aio(&cb_flag)) != 0) { 259 TFAIL("aio-mark-keeps", "request finished with %s, " 260 "wanted success", errname(e)); 261 } else if ((rv = aio_return(&cb_flag)) != 2) { 262 TFAIL("aio-mark-keeps", "aio_return gave %zd, wanted 2", rv); 263 } else if (check_fd("aio-mark-keeps", 40, FD_CLOEXEC | FD_CLOFORK)) { 264 TPASS("aio-mark-keeps"); 265 } 266 267 /* closefrom() cancels through the same path */ 268 (void) pipe_at(80); 269 start_aio(&cb_from, 80, buf_from, sizeof (buf_from)); 270 closefrom(80); 271 if ((e = await_aio(&cb_from)) != ECANCELED) { 272 TFAIL("aio-cancel-closefrom", "request finished with %s, " 273 "wanted ECANCELED", errname(e)); 274 } else if (check_fd("aio-cancel-closefrom", 80, -1)) { 275 TPASS("aio-cancel-closefrom"); 276 } 277 } 278 279 int 280 main(void) 281 { 282 open_fds(); 283 284 /* Invalid arguments. An empty high range so a bug cannot bite. */ 285 expect_einval("low-gt-high", 5, 4, 0); 286 expect_einval("flag-unknown", 1000000, 1000001, 1 << 2); 287 expect_einval("flag-junk", 1000000, 1000001, ~0); 288 289 /* A range beyond every open descriptor succeeds and does nothing */ 290 if (close_range(1000000, UINT_MAX, 0) != 0) { 291 TFAIL("range-empty", "failed: %s", strerrorname_np(errno)); 292 } else { 293 TPASS("range-empty"); 294 } 295 296 /* A low bound above INT_MAX must not close anything */ 297 if (close_range(0x80000000U, UINT_MAX, 0) != 0) { 298 TFAIL("low-gt-intmax", "failed: %s", 299 strerrorname_np(errno)); 300 } else if (check_range("low-gt-intmax", FD_BASE, FD_LIMIT - 1, 0)) { 301 TPASS("low-gt-intmax"); 302 } 303 304 /* Close the middle of the block and check the neighbours */ 305 if (close_range(55, 59, 0) != 0) 306 err(EXIT_FAILURE, "close_range(55, 59, 0)"); 307 if (check_range("close-mid", 50, 54, 0) && 308 check_range("close-mid", 55, 59, -1) && 309 check_range("close-mid", 60, 69, 0)) { 310 TPASS("close-mid"); 311 } 312 313 /* Mark ranges with each flag, then both together */ 314 if (close_range(60, 64, CLOSE_RANGE_CLOEXEC) != 0) 315 err(EXIT_FAILURE, "close_range(CLOSE_RANGE_CLOEXEC)"); 316 if (check_range("mark-cloexec", 60, 64, FD_CLOEXEC) && 317 check_range("mark-cloexec", 65, 69, 0)) { 318 TPASS("mark-cloexec"); 319 } 320 321 if (close_range(65, 67, CLOSE_RANGE_CLOFORK) != 0) 322 err(EXIT_FAILURE, "close_range(CLOSE_RANGE_CLOFORK)"); 323 if (check_range("mark-clofork", 65, 67, FD_CLOFORK) && 324 check_range("mark-clofork", 68, 69, 0)) { 325 TPASS("mark-clofork"); 326 } 327 328 /* Adding a second flag must preserve the first */ 329 if (close_range(65, 67, CLOSE_RANGE_CLOEXEC) != 0) 330 err(EXIT_FAILURE, "close_range(add CLOSE_RANGE_CLOEXEC)"); 331 if (check_range("mark-accumulate", 65, 67, FD_CLOEXEC | FD_CLOFORK)) 332 TPASS("mark-accumulate"); 333 334 if (close_range(68, 69, 335 CLOSE_RANGE_CLOEXEC | CLOSE_RANGE_CLOFORK) != 0) { 336 err(EXIT_FAILURE, "close_range(both flags)"); 337 } 338 if (check_range("mark-both", 68, 69, FD_CLOEXEC | FD_CLOFORK)) 339 TPASS("mark-both"); 340 341 /* Marking a range that includes closed descriptors skips them */ 342 if (close_range(55, 64, CLOSE_RANGE_CLOEXEC) != 0) 343 err(EXIT_FAILURE, "close_range over closed fds"); 344 if (check_range("mark-gaps", 55, 59, -1) && 345 check_range("mark-gaps", 60, 64, FD_CLOEXEC)) { 346 TPASS("mark-gaps"); 347 } 348 349 /* The closefrom() form: everything at or above low goes */ 350 if (close_range(52, UINT_MAX, 0) != 0) 351 err(EXIT_FAILURE, "close_range(52, UINT_MAX, 0)"); 352 if (check_range("close-from", 50, 51, 0) && 353 check_range("close-from", 52, FD_LIMIT - 1, -1)) { 354 TPASS("close-from"); 355 } 356 357 /* And closefrom(3C) itself, now built on close_range() */ 358 open_fds(); 359 closefrom(FD_BASE + 1); 360 if (check_fd("closefrom", FD_BASE, 0) && 361 check_range("closefrom", FD_BASE + 1, FD_LIMIT - 1, -1)) { 362 TPASS("closefrom"); 363 } 364 365 run_aio_tests(); 366 367 if (failures == 0) { 368 (void) printf("All tests passed\n"); 369 return (EXIT_SUCCESS); 370 } 371 372 (void) fprintf(stderr, "%u test(s) failed\n", failures); 373 return (EXIT_FAILURE); 374 } 375