1 // SPDX-License-Identifier: GPL-2.0
2 /*
3 * An unmounted mount that would have stayed attached to its unmounted parent
4 * leaves a cover behind instead. A lookup on the parent at the mountpoint
5 * finds knullfs: an empty read-only directory that is shared by every cover
6 * and every kernel thread, so it can't be watched or locked and nothing can
7 * be mounted on it. The mount itself is a root from then on. Where a file
8 * was mounted, the stand-in is an empty regular file of the same instance.
9 */
10 #define _GNU_SOURCE
11 #include <errno.h>
12 #include <fcntl.h>
13 #include <sched.h>
14 #include <stdint.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <unistd.h>
19 #include <sys/fanotify.h>
20 #include <sys/file.h>
21 #include <sys/inotify.h>
22 #include <sys/mount.h>
23 #include <sys/prctl.h>
24 #include <sys/stat.h>
25 #include <sys/statvfs.h>
26 #include <sys/syscall.h>
27 #include <sys/vfs.h>
28 #include <sys/wait.h>
29
30 #include "../../kselftest_harness.h"
31 #include "../readdir_hold.h"
32
33 #ifndef NULL_FS_MAGIC
34 #define NULL_FS_MAGIC 0x4E554C4C
35 #endif
36
37 #ifndef TMPFS_MAGIC
38 #define TMPFS_MAGIC 0x01021994
39 #endif
40
41 #ifndef F_SETDELEG
42 #define F_SETDELEG (F_SETLEASE + 16)
43 #endif
44
45 /* struct delegation of <linux/fcntl.h>, which doesn't mix with <fcntl.h> */
46 struct delegation_req {
47 uint32_t d_flags;
48 uint16_t d_type;
49 uint16_t __pad;
50 };
51
52 #define DIR_LEN 64
53 #define PATH_LEN 128
54
55 /* what the parent asks the child to do */
56 #define CMD_RMDIR 'r'
57 #define CMD_CLOSE_T 't'
58 #define CMD_QUIT 'q'
59
write_file(const char * path,const char * s)60 static int write_file(const char *path, const char *s)
61 {
62 ssize_t n = -1;
63 int fd;
64
65 fd = open(path, O_WRONLY | O_CLOEXEC);
66 if (fd >= 0) {
67 n = write(fd, s, strlen(s));
68 close(fd);
69 }
70 return n == (ssize_t)strlen(s) ? 0 : -1;
71 }
72
touch(const char * path)73 static int touch(const char *path)
74 {
75 int fd;
76
77 fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
78 if (fd < 0)
79 return -1;
80 close(fd);
81 return 0;
82 }
83
84 /* Become root in a new user namespace with a private mount namespace. */
enter_userns(void)85 static int enter_userns(void)
86 {
87 uid_t uid = getuid();
88 gid_t gid = getgid();
89 char map[32];
90
91 prctl(PR_SET_DUMPABLE, 1);
92 if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
93 return -1;
94 if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
95 return -1;
96 snprintf(map, sizeof(map), "0 %d 1", uid);
97 if (write_file("/proc/self/uid_map", map))
98 return -1;
99 snprintf(map, sizeof(map), "0 %d 1", gid);
100 if (write_file("/proc/self/gid_map", map))
101 return -1;
102 if (setgid(0) || setuid(0))
103 return -1;
104 return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL);
105 }
106
107 /*
108 * The child mounts P on @base/p, C on P/covered and the file P/src on P/file
109 * in a mount namespace of its own, binds P a second time at @base/q and hands
110 * out descriptors on P and on C. rmdir() of @base/p from here unmounts P
111 * together with C and the file bind. P and C are held by the descriptors and
112 * the unmounted children leave their covers behind. On request the child
113 * removes C's mountpoint through the bind.
114 *
115 * It also mounts T on @base/t with a child on T/covered, binds T at @base/u
116 * with a second child on the same dentry and hands out descriptors on T and
117 * U. rmdir() of both from here leaves two covers on one mountpoint. On request
118 * the child lets go of T.
119 */
cover_child(const char * base,int to_parent,int from_parent)120 static int cover_child(const char *base, int to_parent, int from_parent)
121 {
122 char p[PATH_LEN], c[PATH_LEN], q[PATH_LEN], qc[PATH_LEN];
123 char f[PATH_LEN], src[PATH_LEN], t[PATH_LEN], u[PATH_LEN], tc[PATH_LEN];
124 int fds[4], ret;
125 char cmd;
126
127 if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
128 return 1;
129 snprintf(p, sizeof(p), "%s/p", base);
130 if (mkdir(p, 0755) || mount("tmpfs", p, "tmpfs", 0, NULL))
131 return 2;
132 snprintf(c, sizeof(c), "%s/p/covered", base);
133 if (mkdir(c, 0755) || mount("tmpfs", c, "tmpfs", 0, NULL))
134 return 3;
135 snprintf(f, sizeof(f), "%s/p/file", base);
136 snprintf(src, sizeof(src), "%s/p/src", base);
137 if (touch(src) || touch(f) || mount(src, f, NULL, MS_BIND, NULL))
138 return 4;
139 snprintf(q, sizeof(q), "%s/q", base);
140 if (mkdir(q, 0755) || mount(p, q, NULL, MS_BIND, NULL))
141 return 5;
142 snprintf(t, sizeof(t), "%s/t", base);
143 if (mkdir(t, 0755) || mount("tmpfs", t, "tmpfs", 0, NULL))
144 return 6;
145 snprintf(tc, sizeof(tc), "%s/t/covered", base);
146 if (mkdir(tc, 0755) || mount("tmpfs", tc, "tmpfs", 0, NULL))
147 return 7;
148 snprintf(u, sizeof(u), "%s/u", base);
149 if (mkdir(u, 0755) || mount(t, u, NULL, MS_BIND, NULL))
150 return 8;
151 snprintf(tc, sizeof(tc), "%s/u/covered", base);
152 if (mount("tmpfs", tc, "tmpfs", 0, NULL))
153 return 9;
154 fds[0] = open(p, O_PATH | O_DIRECTORY | O_CLOEXEC);
155 fds[1] = open(c, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
156 fds[2] = open(t, O_PATH | O_DIRECTORY | O_CLOEXEC);
157 fds[3] = open(u, O_PATH | O_DIRECTORY | O_CLOEXEC);
158 if (fds[0] < 0 || fds[1] < 0 || fds[2] < 0 || fds[3] < 0)
159 return 10;
160 if (write(to_parent, fds, sizeof(fds)) != sizeof(fds))
161 return 11;
162
163 snprintf(qc, sizeof(qc), "%s/q/covered", base);
164 for (;;) {
165 if (read(from_parent, &cmd, 1) != 1)
166 return 12;
167 switch (cmd) {
168 case CMD_RMDIR:
169 ret = rmdir(qc) ? errno : 0;
170 if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret))
171 return 13;
172 break;
173 case CMD_CLOSE_T:
174 ret = close(fds[2]) ? errno : 0;
175 if (write(to_parent, &ret, sizeof(ret)) != sizeof(ret))
176 return 13;
177 break;
178 case CMD_QUIT:
179 return 0;
180 default:
181 return 14;
182 }
183 }
184 }
185
FIXTURE(mount_cover)186 FIXTURE(mount_cover) {
187 char base[DIR_LEN];
188 pid_t child;
189 int to_child;
190 int from_child;
191 int dfd; /* P, unmounted, held */
192 int cfd; /* C, unmounted, held */
193 int fd; /* what a lookup on P finds at C's mountpoint */
194 int tfd; /* T, unmounted, held */
195 int ufd; /* U, a bind of T, unmounted, held */
196 int fan; /* fanotify group from before the user namespace, or -1 */
197 struct readdir_hold hold; /* likewise from before, uffd -1 without */
198 };
199
200 /*
201 * Everything the setup has made. The harness does not run the teardown
202 * when an assertion of the setup fails, so the setup calls this itself.
203 */
cover_cleanup(FIXTURE_DATA (mount_cover)* self)204 static void cover_cleanup(FIXTURE_DATA(mount_cover) *self)
205 {
206 char cmd = CMD_QUIT;
207 int status;
208
209 if (self->fd >= 0)
210 close(self->fd);
211 if (self->cfd >= 0)
212 close(self->cfd);
213 if (self->dfd >= 0)
214 close(self->dfd);
215 if (self->tfd >= 0)
216 close(self->tfd);
217 if (self->ufd >= 0)
218 close(self->ufd);
219 if (self->fan >= 0)
220 close(self->fan);
221 readdir_hold_destroy(&self->hold);
222 if (self->child > 0) {
223 if (write(self->to_child, &cmd, 1) != 1)
224 kill(self->child, SIGKILL);
225 waitpid(self->child, &status, 0);
226 }
227 if (self->to_child >= 0)
228 close(self->to_child);
229 if (self->from_child >= 0)
230 close(self->from_child);
231 umount2(self->base, MNT_DETACH);
232 rmdir(self->base);
233 }
234
same_file(int fd1,int fd2)235 static int same_file(int fd1, int fd2)
236 {
237 struct stat st1, st2;
238
239 if (fstat(fd1, &st1) || fstat(fd2, &st2))
240 return 0;
241 return st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino;
242 }
243
FIXTURE_SETUP(mount_cover)244 FIXTURE_SETUP(mount_cover)
245 {
246 int to_parent[2], to_child[2], fds[4], pidfd, status;
247 char dir[PATH_LEN];
248 struct statfs sf;
249
250 self->child = 0;
251 self->to_child = self->from_child = -1;
252 self->dfd = self->cfd = self->fd = self->tfd = self->ufd = -1;
253
254 /*
255 * A group for plain events takes CAP_SYS_ADMIN in the initial user
256 * namespace, so get one before that is gone. An inode mark can be
257 * added to it from anywhere.
258 */
259 self->fan = fanotify_init(FAN_CLASS_NOTIF | FAN_CLOEXEC, O_RDONLY);
260 /* same for the userfaultfd that holds a readdir in its fault */
261 readdir_hold_init(&self->hold);
262
263 snprintf(self->base, sizeof(self->base), "/tmp/mount_cover.XXXXXX");
264 ASSERT_NE(mkdtemp(self->base), NULL);
265 if (enter_userns()) {
266 cover_cleanup(self);
267 SKIP(return, "test requires user namespaces");
268 }
269 ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0)
270 cover_cleanup(self);
271
272 snprintf(dir, sizeof(dir), "%s/p", self->base);
273 ASSERT_EQ(pipe(to_parent), 0)
274 cover_cleanup(self);
275 ASSERT_EQ(pipe(to_child), 0)
276 cover_cleanup(self);
277 self->child = fork();
278 ASSERT_GE(self->child, 0)
279 cover_cleanup(self);
280 if (self->child == 0) {
281 close(to_parent[0]);
282 close(to_child[1]);
283 _exit(cover_child(self->base, to_parent[1], to_child[0]));
284 }
285 close(to_parent[1]);
286 close(to_child[0]);
287 self->to_child = to_child[1];
288 self->from_child = to_parent[0];
289 if (read(self->from_child, fds, sizeof(fds)) != sizeof(fds)) {
290 pid_t pid = self->child;
291
292 waitpid(pid, &status, 0);
293 self->child = 0;
294 cover_cleanup(self);
295 ASSERT_TRUE(false)
296 TH_LOG("child failed to set up: %s %d",
297 WIFEXITED(status) ? "step" : "signal",
298 WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status));
299 }
300
301 pidfd = syscall(__NR_pidfd_open, self->child, 0);
302 ASSERT_GE(pidfd, 0)
303 cover_cleanup(self);
304 self->dfd = syscall(__NR_pidfd_getfd, pidfd, fds[0], 0);
305 self->cfd = syscall(__NR_pidfd_getfd, pidfd, fds[1], 0);
306 self->tfd = syscall(__NR_pidfd_getfd, pidfd, fds[2], 0);
307 self->ufd = syscall(__NR_pidfd_getfd, pidfd, fds[3], 0);
308 close(pidfd);
309 ASSERT_GE(self->dfd, 0)
310 cover_cleanup(self);
311 ASSERT_GE(self->cfd, 0)
312 cover_cleanup(self);
313 ASSERT_GE(self->tfd, 0)
314 cover_cleanup(self);
315 ASSERT_GE(self->ufd, 0)
316 cover_cleanup(self);
317
318 /* unmounts P and C, C leaves its cover behind */
319 ASSERT_EQ(rmdir(dir), 0)
320 cover_cleanup(self);
321 /* unmounts T and U with their children, two covers on one mountpoint */
322 snprintf(dir, sizeof(dir), "%s/t", self->base);
323 ASSERT_EQ(rmdir(dir), 0)
324 cover_cleanup(self);
325 snprintf(dir, sizeof(dir), "%s/u", self->base);
326 ASSERT_EQ(rmdir(dir), 0)
327 cover_cleanup(self);
328
329 self->fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
330 ASSERT_GE(self->fd, 0)
331 cover_cleanup(self);
332 ASSERT_EQ(fstatfs(self->fd, &sf), 0)
333 cover_cleanup(self);
334 ASSERT_EQ(sf.f_type, NULL_FS_MAGIC)
335 cover_cleanup(self);
336 }
337
FIXTURE_TEARDOWN(mount_cover)338 FIXTURE_TEARDOWN(mount_cover)
339 {
340 cover_cleanup(self);
341 }
342
TEST_F(mount_cover,not_watchable)343 TEST_F(mount_cover, not_watchable)
344 {
345 char p[PATH_LEN];
346 int ifd;
347
348 snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd);
349
350 ifd = inotify_init1(IN_CLOEXEC);
351 if (ifd < 0 && errno == ENOSYS) {
352 TH_LOG("no inotify in this kernel, skipping that part");
353 } else {
354 ASSERT_GE(ifd, 0);
355 EXPECT_EQ(inotify_add_watch(ifd, p, IN_OPEN), -1);
356 EXPECT_EQ(errno, EINVAL);
357 close(ifd);
358 }
359
360 /* dnotify ends up at the same place */
361 EXPECT_EQ(fcntl(self->fd, F_NOTIFY, DN_ACCESS), -1);
362 EXPECT_EQ(errno, EINVAL);
363
364 if (self->fan < 0) {
365 TH_LOG("no fanotify group without CAP_SYS_ADMIN in the initial user namespace, skipping the fanotify part");
366 return;
367 }
368 EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD, FAN_OPEN, self->fd, NULL), -1);
369 EXPECT_EQ(errno, EINVAL);
370 }
371
TEST_F(mount_cover,not_lockable)372 TEST_F(mount_cover, not_lockable)
373 {
374 struct flock fl = {
375 .l_type = F_RDLCK,
376 .l_whence = SEEK_SET,
377 };
378
379 EXPECT_EQ(flock(self->fd, LOCK_EX | LOCK_NB), -1);
380 EXPECT_EQ(errno, ENOLCK);
381 EXPECT_EQ(fcntl(self->fd, F_SETLK, &fl), -1);
382 EXPECT_EQ(errno, ENOLCK);
383 EXPECT_EQ(fcntl(self->fd, F_GETLK, &fl), -1);
384 EXPECT_EQ(errno, ENOLCK);
385 }
386
387 /* a lease is refused for the owner and for everybody else alike */
TEST_F(mount_cover,not_leasable)388 TEST_F(mount_cover, not_leasable)
389 {
390 struct delegation_req deleg = {
391 .d_type = F_RDLCK,
392 };
393
394 EXPECT_EQ(fcntl(self->fd, F_SETLEASE, F_RDLCK), -1);
395 EXPECT_TRUE(errno == EINVAL || errno == EACCES);
396 EXPECT_EQ(fcntl(self->fd, F_SETDELEG, &deleg), -1);
397 EXPECT_TRUE(errno == EINVAL || errno == EACCES);
398 }
399
TEST_F(mount_cover,not_mountable)400 TEST_F(mount_cover, not_mountable)
401 {
402 char p[PATH_LEN];
403
404 snprintf(p, sizeof(p), "/proc/self/fd/%d", self->fd);
405 EXPECT_EQ(mount("tmpfs", p, "tmpfs", 0, NULL), -1);
406 EXPECT_EQ(errno, ENOENT);
407 }
408
TEST_F(mount_cover,read_only)409 TEST_F(mount_cover, read_only)
410 {
411 struct statvfs sv;
412
413 EXPECT_EQ(mkdirat(self->fd, "x", 0755), -1);
414 EXPECT_EQ(errno, ENOENT);
415 EXPECT_EQ(fchmod(self->fd, 0777), -1);
416 EXPECT_EQ(errno, EROFS);
417 /* the immutable inode is checked before the read-only mount */
418 EXPECT_EQ(faccessat(self->fd, ".", W_OK, 0), -1);
419 EXPECT_EQ(errno, EPERM);
420 ASSERT_EQ(fstatvfs(self->fd, &sv), 0);
421 EXPECT_TRUE(sv.f_flag & ST_RDONLY);
422 }
423
424 /* the stand-in is a root of its own, ".." stays put */
TEST_F(mount_cover,island)425 TEST_F(mount_cover, island)
426 {
427 int fd;
428
429 fd = openat(self->fd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
430 ASSERT_GE(fd, 0);
431 EXPECT_TRUE(same_file(fd, self->fd));
432 close(fd);
433 }
434
435 /*
436 * C is alive for as long as the child holds it, but it can't be reached
437 * through P anymore and it's a root of its own as well.
438 */
TEST_F(mount_cover,held_child_detached)439 TEST_F(mount_cover, held_child_detached)
440 {
441 struct statfs sf;
442 int fd;
443
444 ASSERT_EQ(fstatfs(self->cfd, &sf), 0);
445 EXPECT_EQ(sf.f_type, TMPFS_MAGIC);
446
447 fd = openat(self->cfd, "..", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
448 ASSERT_GE(fd, 0);
449 EXPECT_TRUE(same_file(fd, self->cfd));
450 close(fd);
451
452 fd = openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
453 ASSERT_GE(fd, 0);
454 ASSERT_EQ(fstatfs(fd, &sf), 0);
455 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
456 close(fd);
457 }
458
459 /*
460 * The cover goes with its mountpoint: once the child has removed C's
461 * mountpoint through the bind of P, the name is gone from P as well.
462 * What was opened through the cover before stays open.
463 */
TEST_F(mount_cover,cover_goes_with_mountpoint)464 TEST_F(mount_cover, cover_goes_with_mountpoint)
465 {
466 char cmd = CMD_RMDIR;
467 struct statfs sf;
468 int ret;
469
470 ASSERT_EQ(write(self->to_child, &cmd, 1), 1);
471 ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret));
472 ASSERT_EQ(ret, 0);
473
474 EXPECT_EQ(openat(self->dfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1);
475 EXPECT_EQ(errno, ENOENT);
476 ASSERT_EQ(fstatfs(self->fd, &sf), 0);
477 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
478 }
479
480 /*
481 * A readdir of the stand-in holds nothing that others wait for: one holder
482 * sticks in the page fault of its buffer, and a create and a lookup of
483 * another come back meanwhile.
484 */
TEST_F(mount_cover,readdir_blocks_nobody)485 TEST_F(mount_cover, readdir_blocks_nobody)
486 {
487 bool stalled;
488
489 if (self->hold.uffd < 0)
490 SKIP(return, "test requires userfaultfd");
491 ASSERT_EQ(readdir_hold_check(&self->hold, self->fd, &stalled), 0);
492 EXPECT_FALSE(stalled);
493 }
494
495 /* where a file was mounted, the stand-in is an empty regular file */
TEST_F(mount_cover,file_stand_in)496 TEST_F(mount_cover, file_stand_in)
497 {
498 char src[PATH_LEN], p[PATH_LEN];
499 struct statfs sf;
500 struct stat st;
501 char c;
502 int fd;
503
504 fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC);
505 ASSERT_GE(fd, 0);
506 ASSERT_EQ(fstat(fd, &st), 0);
507 EXPECT_TRUE(S_ISREG(st.st_mode));
508 ASSERT_EQ(fstatfs(fd, &sf), 0);
509 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
510 /* the two stand-ins are two inodes */
511 EXPECT_FALSE(same_file(fd, self->fd));
512 EXPECT_EQ(read(fd, &c, 1), 0);
513 EXPECT_EQ(flock(fd, LOCK_EX | LOCK_NB), -1);
514 EXPECT_EQ(errno, ENOLCK);
515
516 snprintf(src, sizeof(src), "%s/src", self->base);
517 ASSERT_EQ(touch(src), 0);
518 snprintf(p, sizeof(p), "/proc/self/fd/%d", fd);
519 EXPECT_EQ(mount(src, p, NULL, MS_BIND, NULL), -1);
520 EXPECT_EQ(errno, ENOENT);
521 close(fd);
522
523 /* writes and truncates go nowhere, like /dev/null */
524 fd = openat(self->dfd, "file", O_WRONLY | O_TRUNC | O_CLOEXEC);
525 ASSERT_GE(fd, 0);
526 EXPECT_EQ(write(fd, "xy", 2), 2);
527 EXPECT_EQ(ftruncate(fd, 4), 0);
528 ASSERT_EQ(fstat(fd, &st), 0);
529 EXPECT_EQ(st.st_size, 0);
530 /* the shared inode keeps its identity */
531 EXPECT_EQ(fchmod(fd, 0600), -1);
532 EXPECT_EQ(errno, EPERM);
533 close(fd);
534 fd = openat(self->dfd, "file", O_RDONLY | O_CLOEXEC);
535 ASSERT_GE(fd, 0);
536 EXPECT_EQ(read(fd, &c, 1), 0);
537 close(fd);
538
539 EXPECT_EQ(openat(self->dfd, "file", O_RDONLY | O_DIRECTORY | O_CLOEXEC), -1);
540 EXPECT_EQ(errno, ENOTDIR);
541 }
542
543 /*
544 * Two unmounted parents left covers on the same dentry. A lookup on
545 * either finds a stand-in, and U's cover stays when T goes.
546 */
TEST_F(mount_cover,shared_mountpoint)547 TEST_F(mount_cover, shared_mountpoint)
548 {
549 char cmd = CMD_CLOSE_T;
550 struct statfs sf;
551 int fd, ret;
552
553 fd = openat(self->tfd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
554 ASSERT_GE(fd, 0);
555 ASSERT_EQ(fstatfs(fd, &sf), 0);
556 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
557 close(fd);
558
559 fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
560 ASSERT_GE(fd, 0);
561 ASSERT_EQ(fstatfs(fd, &sf), 0);
562 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
563 close(fd);
564
565 /* the last references to T go, with T its cover */
566 ASSERT_EQ(write(self->to_child, &cmd, 1), 1);
567 ASSERT_EQ(read(self->from_child, &ret, sizeof(ret)), sizeof(ret));
568 ASSERT_EQ(ret, 0);
569 close(self->tfd);
570 self->tfd = -1;
571
572 fd = openat(self->ufd, "covered", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
573 ASSERT_GE(fd, 0);
574 ASSERT_EQ(fstatfs(fd, &sf), 0);
575 EXPECT_EQ(sf.f_type, NULL_FS_MAGIC);
576 close(fd);
577 }
578
579 TEST_HARNESS_MAIN
580