xref: /illumos-gate/usr/src/test/os-tests/tests/close_range.c (revision 5400c85432c0a689ecefb8c19b8f9861809ca093)
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