xref: /linux/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c (revision 1dbb7422053fa0f4fb5f9acc3931617fc0091939)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * MNT_LOCKED keeps the owner of a user namespace from revealing what a
4  * mount covers. The lock has to be set on the copy in that namespace and
5  * only there, it has to survive the expiry of a mount placed beneath the
6  * locked one, and it has to survive the propagated umount of such a copy.
7  */
8 #define _GNU_SOURCE
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <sched.h>
12 #include <stdbool.h>
13 #include <signal.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17 #include <unistd.h>
18 #include <sys/mount.h>
19 #include <sys/prctl.h>
20 #include <sys/stat.h>
21 #include <sys/syscall.h>
22 #include <sys/wait.h>
23 
24 #include "../../kselftest_harness.h"
25 
26 #ifndef MOVE_MOUNT_F_EMPTY_PATH
27 #define MOVE_MOUNT_F_EMPTY_PATH	0x00000004
28 #endif
29 #ifndef MOVE_MOUNT_BENEATH
30 #define MOVE_MOUNT_BENEATH	0x00000200
31 #endif
32 #ifndef OPEN_TREE_CLONE
33 #define OPEN_TREE_CLONE		1
34 #endif
35 #ifndef OPEN_TREE_CLOEXEC
36 #define OPEN_TREE_CLOEXEC	O_CLOEXEC
37 #endif
38 
39 #define DIR_LEN		64
40 #define PATH_LEN	192
41 #define FLAGS		(MS_NOSUID | MS_NODEV | MS_NOEXEC)
42 
43 /* exit codes of the children, each test says what they mean */
44 enum {
45 	CHILD_OK,
46 	CHILD_SETUP,
47 	CHILD_STEP1,
48 	CHILD_STEP2,
49 	CHILD_STEP3,
50 	CHILD_STEP4,
51 };
52 
write_file(const char * path,const char * s)53 static int write_file(const char *path, const char *s)
54 {
55 	ssize_t n = -1;
56 	int fd;
57 
58 	fd = open(path, O_WRONLY | O_CLOEXEC);
59 	if (fd >= 0) {
60 		n = write(fd, s, strlen(s));
61 		close(fd);
62 	}
63 	return n == (ssize_t)strlen(s) ? 0 : -1;
64 }
65 
create_file(const char * path,const char * s)66 static int create_file(const char *path, const char *s)
67 {
68 	ssize_t n = -1;
69 	int fd;
70 
71 	fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
72 	if (fd >= 0) {
73 		n = write(fd, s, strlen(s));
74 		close(fd);
75 	}
76 	return n == (ssize_t)strlen(s) ? 0 : -1;
77 }
78 
79 /* Root in a new user namespace with a copy of the mount namespace. */
enter_userns(void)80 static int enter_userns(void)
81 {
82 	uid_t uid = getuid();
83 	gid_t gid = getgid();
84 	char map[32];
85 
86 	prctl(PR_SET_DUMPABLE, 1);
87 	if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
88 		return -1;
89 	if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
90 		return -1;
91 	snprintf(map, sizeof(map), "0 %d 1", uid);
92 	if (write_file("/proc/self/uid_map", map))
93 		return -1;
94 	snprintf(map, sizeof(map), "0 %d 1", gid);
95 	if (write_file("/proc/self/gid_map", map))
96 		return -1;
97 	return setgid(0) || setuid(0) ? -1 : 0;
98 }
99 
wait_child(pid_t pid)100 static int wait_child(pid_t pid)
101 {
102 	int status;
103 
104 	if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
105 		return -1;
106 	return WEXITSTATUS(status);
107 }
108 
wait_byte(int fd)109 static int wait_byte(int fd)
110 {
111 	char c;
112 
113 	return read(fd, &c, 1) == 1 ? 0 : -1;
114 }
115 
send_byte(int fd)116 static int send_byte(int fd)
117 {
118 	return write(fd, "x", 1) == 1 ? 0 : -1;
119 }
120 
121 /* A read of @path fails: the file is covered. */
covered(const char * path)122 static bool covered(const char *path)
123 {
124 	int fd = open(path, O_RDONLY | O_CLOEXEC);
125 
126 	if (fd >= 0)
127 		close(fd);
128 	return fd < 0;
129 }
130 
131 /* What @path reads, "" if it can't be read. */
read_word(const char * path,char * buf,size_t len)132 static int read_word(const char *path, char *buf, size_t len)
133 {
134 	ssize_t n = 0;
135 	int fd;
136 
137 	fd = open(path, O_RDONLY | O_CLOEXEC);
138 	if (fd >= 0) {
139 		n = read(fd, buf, len - 1);
140 		close(fd);
141 	}
142 	buf[n > 0 ? n : 0] = 0;
143 	return fd < 0 ? -1 : 0;
144 }
145 
146 /* Put a clone of the mount at @src beneath the top mount at @where. */
move_beneath(const char * src,const char * where)147 static int move_beneath(const char *src, const char *where)
148 {
149 	int fd, ret;
150 
151 	fd = syscall(__NR_open_tree, AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
152 	if (fd < 0)
153 		return -1;
154 	ret = syscall(__NR_move_mount, fd, "", AT_FDCWD, where,
155 		      MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
156 	close(fd);
157 	return ret;
158 }
159 
FIXTURE(locked_mount)160 FIXTURE(locked_mount) {
161 	char base[DIR_LEN];
162 	bool tracing;		/* debugfs with the tracefs automount is there */
163 };
164 
FIXTURE_SETUP(locked_mount)165 FIXTURE_SETUP(locked_mount)
166 {
167 	char p[PATH_LEN];
168 	struct stat st;
169 
170 	if (geteuid())
171 		SKIP(return, "test requires root");
172 
173 	snprintf(self->base, sizeof(self->base), "/tmp/locked_mount.XXXXXX");
174 	ASSERT_NE(mkdtemp(self->base), NULL);
175 	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
176 	ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
177 	ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, "mode=0755"), 0);
178 
179 	/* an automount that a user can name: the tracefs below debugfs */
180 	snprintf(p, sizeof(p), "%s/dbg", self->base);
181 	ASSERT_EQ(mkdir(p, 0755), 0);
182 	self->tracing = !mount("debugfs", p, "debugfs", FLAGS, NULL);
183 	if (self->tracing) {
184 		/* the cases trigger it themselves */
185 		snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
186 		self->tracing = !fstatat(AT_FDCWD, p, &st, AT_NO_AUTOMOUNT) &&
187 				S_ISDIR(st.st_mode);
188 	}
189 }
190 
FIXTURE_TEARDOWN(locked_mount)191 FIXTURE_TEARDOWN(locked_mount)
192 {
193 	umount2(self->base, MNT_DETACH);
194 	rmdir(self->base);
195 }
196 
197 /*
198  * The child keeps a directory descriptor on the host's debugfs mount, moves
199  * to a user namespace of its own and triggers the automount through the
200  * descriptor. The mount goes below the host's mount and propagates into the
201  * child's copy. The child's copy has to be locked, the host's not.
202  */
automount_child(const char * base,int dfd,int to_host,int from_host)203 static int automount_child(const char *base, int dfd, int to_host, int from_host)
204 {
205 	char p[PATH_LEN];
206 	struct stat st;
207 
208 	if (enter_userns())
209 		return CHILD_SETUP;
210 	if (fstatat(dfd, "tracing/.", &st, 0))
211 		return CHILD_STEP1;
212 	if (send_byte(to_host) || wait_byte(from_host))
213 		return CHILD_SETUP;
214 	/* the flags of the copy are locked: EPERM */
215 	snprintf(p, sizeof(p), "%s/dbg/tracing", base);
216 	if (!mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL) || errno != EPERM)
217 		return CHILD_STEP2;
218 	return CHILD_OK;
219 }
220 
TEST_F(locked_mount,automount_locked_in_the_triggering_namespace)221 TEST_F(locked_mount, automount_locked_in_the_triggering_namespace)
222 {
223 	int to_host[2], from_host[2], dfd, ret;
224 	char p[PATH_LEN];
225 	pid_t pid;
226 
227 	if (!self->tracing)
228 		SKIP(return, "test requires debugfs with the tracefs automount");
229 
230 	snprintf(p, sizeof(p), "%s/dbg", self->base);
231 	ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
232 	dfd = open(p, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
233 	ASSERT_GE(dfd, 0);
234 	ASSERT_EQ(pipe(to_host), 0);
235 	ASSERT_EQ(pipe(from_host), 0);
236 
237 	pid = fork();
238 	ASSERT_GE(pid, 0);
239 	if (pid == 0)
240 		_exit(automount_child(self->base, dfd, to_host[1], from_host[0]));
241 	close(dfd);
242 	ASSERT_EQ(wait_byte(to_host[0]), 0);
243 
244 	/* the host's own mount isn't locked: the flags can go */
245 	snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
246 	EXPECT_EQ(mount(NULL, p, NULL, MS_REMOUNT | MS_BIND, NULL), 0);
247 
248 	ASSERT_EQ(send_byte(from_host[1]), 0);
249 	ret = wait_child(pid);
250 	TH_LOG("child exit code %d", ret);
251 	EXPECT_EQ(ret, CHILD_OK);
252 }
253 
254 /*
255  * A copy of a tree with a locked cover. The child puts a shrinkable mount
256  * beneath the cover, which hands the lock down, unmounts the cover and then
257  * asks for the umount of an unlocked ancestor. That expires the shrinkable
258  * mount on a kernel that doesn't look at the lock, and the covered
259  * directory is bare.
260  */
expiry_child(const char * base)261 static int expiry_child(const char *base)
262 {
263 	char srv[PATH_LEN], shr[PATH_LEN], x[PATH_LEN], c[PATH_LEN];
264 	char hidden[PATH_LEN], secret[PATH_LEN];
265 
266 	snprintf(srv, sizeof(srv), "%s/srv", base);
267 	snprintf(shr, sizeof(shr), "%s/shr", base);
268 	snprintf(x, sizeof(x), "%s/x", base);
269 	snprintf(c, sizeof(c), "%s/c", base);
270 	snprintf(hidden, sizeof(hidden), "%s/x/hidden", base);
271 	snprintf(secret, sizeof(secret), "%s/x/hidden/secret", base);
272 
273 	if (enter_userns())
274 		return CHILD_SETUP;
275 	if (mount(srv, x, NULL, MS_BIND | MS_REC, NULL) ||
276 	    mount(shr, c, NULL, MS_BIND, NULL))
277 		return CHILD_SETUP;
278 	/* the cover is locked */
279 	if (!umount2(hidden, 0) || errno != EINVAL)
280 		return CHILD_STEP1;
281 	if (syscall(__NR_move_mount, AT_FDCWD, c, AT_FDCWD, hidden, MOVE_MOUNT_BENEATH))
282 		return CHILD_STEP2;
283 	/* the lock moved down, the cover may go */
284 	if (umount2(hidden, 0))
285 		return CHILD_STEP3;
286 	/* the holder of the lock may not, in any way */
287 	if (!umount2(hidden, 0) || errno != EINVAL)
288 		return CHILD_STEP4;
289 	if (chdir(x))
290 		return CHILD_SETUP;
291 	umount2(x, 0);
292 	return covered(secret) ? CHILD_OK : CHILD_STEP4;
293 }
294 
TEST_F(locked_mount,expiry_leaves_a_locked_mount_alone)295 TEST_F(locked_mount, expiry_leaves_a_locked_mount_alone)
296 {
297 	char p[PATH_LEN], q[PATH_LEN];
298 	pid_t pid;
299 	int ret;
300 
301 	if (!self->tracing)
302 		SKIP(return, "test requires debugfs with the tracefs automount");
303 
304 	snprintf(p, sizeof(p), "%s/dbg/tracing", self->base);
305 	snprintf(q, sizeof(q), "%s/shr", self->base);
306 	ASSERT_EQ(mkdir(q, 0755), 0);
307 	/* a bind of an automount is shrinkable as well */
308 	ASSERT_EQ(mount(p, q, NULL, MS_BIND, NULL), 0);
309 	snprintf(p, sizeof(p), "%s/srv", self->base);
310 	ASSERT_EQ(mkdir(p, 0755), 0);
311 	ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
312 	snprintf(p, sizeof(p), "%s/srv/hidden", self->base);
313 	ASSERT_EQ(mkdir(p, 0755), 0);
314 	snprintf(p, sizeof(p), "%s/srv/hidden/secret", self->base);
315 	ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
316 	snprintf(p, sizeof(p), "%s/srv/hidden", self->base);
317 	ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
318 	snprintf(p, sizeof(p), "%s/x", self->base);
319 	ASSERT_EQ(mkdir(p, 0755), 0);
320 	snprintf(p, sizeof(p), "%s/c", self->base);
321 	ASSERT_EQ(mkdir(p, 0755), 0);
322 
323 	pid = fork();
324 	ASSERT_GE(pid, 0);
325 	if (pid == 0)
326 		_exit(expiry_child(self->base));
327 	ret = wait_child(pid);
328 	TH_LOG("child exit code %d", ret);
329 	EXPECT_EQ(ret, CHILD_OK);
330 }
331 
332 /*
333  * The copy of the mount tree that a user namespace gets at its creation has
334  * the automounts in it locked unless they are on an expiry list. A bind of
335  * such a tree inside the namespace copies the lock to the automount but not
336  * to the root of the bind, so the child may ask for the umount of that root.
337  * That must not expire the locked automount below it: the root is busy, the
338  * umount fails and the automount has to be there afterwards.
339  */
copied_tree_child(const char * base)340 static int copied_tree_child(const char *base)
341 {
342 	char dbg[PATH_LEN], x[PATH_LEN], tracing[PATH_LEN];
343 	struct stat root, st;
344 
345 	snprintf(dbg, sizeof(dbg), "%s/dbg", base);
346 	snprintf(x, sizeof(x), "%s/x", base);
347 	snprintf(tracing, sizeof(tracing), "%s/x/tracing", base);
348 
349 	if (enter_userns())
350 		return CHILD_SETUP;
351 	if (mount(dbg, x, NULL, MS_BIND | MS_REC, NULL))
352 		return CHILD_SETUP;
353 	/* the copy of the automount carries the lock */
354 	if (!umount2(tracing, 0) || errno != EINVAL)
355 		return CHILD_STEP1;
356 	/* the root of the bind doesn't; keep it busy */
357 	if (chdir(x))
358 		return CHILD_SETUP;
359 	if (!umount2(x, 0) || errno != EBUSY)
360 		return CHILD_STEP2;
361 	/* the automount below it must not have gone */
362 	if (stat(x, &root) || fstatat(AT_FDCWD, tracing, &st, AT_NO_AUTOMOUNT))
363 		return CHILD_SETUP;
364 	return st.st_dev != root.st_dev ? CHILD_OK : CHILD_STEP3;
365 }
366 
TEST_F(locked_mount,umount_of_a_bind_leaves_a_locked_automount_alone)367 TEST_F(locked_mount, umount_of_a_bind_leaves_a_locked_automount_alone)
368 {
369 	char p[PATH_LEN];
370 	struct stat st;
371 	pid_t pid;
372 	int ret;
373 
374 	if (!self->tracing)
375 		SKIP(return, "test requires debugfs with the tracefs automount");
376 
377 	/* the copy the child gets has to contain the automount */
378 	snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base);
379 	ASSERT_EQ(stat(p, &st), 0);
380 	snprintf(p, sizeof(p), "%s/x", self->base);
381 	ASSERT_EQ(mkdir(p, 0755), 0);
382 
383 	pid = fork();
384 	ASSERT_GE(pid, 0);
385 	if (pid == 0)
386 		_exit(copied_tree_child(self->base));
387 	ret = wait_child(pid);
388 	TH_LOG("child exit code %d", ret);
389 	EXPECT_EQ(ret, CHILD_OK);
390 }
391 
392 /*
393  * Z is a user namespace with a copy of a tree in which P/d is covered by a
394  * locked mount. The host mounts X on P/d, which propagates beneath Z's
395  * cover, and unmounts it again. Z's cover has to be locked afterwards as
396  * it was before.
397  */
propagation_child(const char * base,int to_host,int from_host)398 static int propagation_child(const char *base, int to_host, int from_host)
399 {
400 	char d[PATH_LEN], secret[PATH_LEN];
401 
402 	snprintf(d, sizeof(d), "%s/P/d", base);
403 	snprintf(secret, sizeof(secret), "%s/P/d/secret", base);
404 
405 	if (enter_userns())
406 		return CHILD_SETUP;
407 	/* the cover is locked */
408 	if (!umount2(d, 0) || errno != EINVAL)
409 		return CHILD_STEP1;
410 	if (send_byte(to_host) || wait_byte(from_host))
411 		return CHILD_SETUP;
412 	/* X came and went beneath it: still locked */
413 	if (!umount2(d, 0) || errno != EINVAL)
414 		return CHILD_STEP2;
415 	return covered(secret) ? CHILD_OK : CHILD_STEP3;
416 }
417 
TEST_F(locked_mount,propagated_copy_keeps_the_cover_locked)418 TEST_F(locked_mount, propagated_copy_keeps_the_cover_locked)
419 {
420 	int to_host[2], from_host[2], ret;
421 	char p[PATH_LEN];
422 	pid_t pid;
423 
424 	snprintf(p, sizeof(p), "%s/P", self->base);
425 	ASSERT_EQ(mkdir(p, 0755), 0);
426 	ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
427 	ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
428 	snprintf(p, sizeof(p), "%s/P/d", self->base);
429 	ASSERT_EQ(mkdir(p, 0755), 0);
430 	snprintf(p, sizeof(p), "%s/P/d/secret", self->base);
431 	ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
432 	ASSERT_EQ(pipe(to_host), 0);
433 	ASSERT_EQ(pipe(from_host), 0);
434 
435 	pid = fork();
436 	ASSERT_GE(pid, 0);
437 	if (pid == 0) {
438 		/* M: a manager's namespace that covers P/d for Z */
439 		pid_t z;
440 
441 		if (unshare(CLONE_NEWNS))
442 			_exit(CHILD_SETUP);
443 		snprintf(p, sizeof(p), "%s/P", self->base);
444 		if (mount(NULL, p, NULL, MS_SLAVE, NULL))
445 			_exit(CHILD_SETUP);
446 		snprintf(p, sizeof(p), "%s/P/d", self->base);
447 		if (mount("tmpfs", p, "tmpfs", 0, "mode=0755"))
448 			_exit(CHILD_SETUP);
449 		z = fork();
450 		if (z < 0)
451 			_exit(CHILD_SETUP);
452 		if (z == 0)
453 			_exit(propagation_child(self->base, to_host[1], from_host[0]));
454 		_exit(wait_child(z));
455 	}
456 	ASSERT_EQ(wait_byte(to_host[0]), 0);
457 
458 	/* the host mounts on P/d and unmounts again; both propagate */
459 	snprintf(p, sizeof(p), "%s/P/d", self->base);
460 	ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
461 	ASSERT_EQ(umount2(p, 0), 0);
462 
463 	ASSERT_EQ(send_byte(from_host[1]), 0);
464 	ret = wait_child(pid);
465 	TH_LOG("child exit code %d", ret);
466 	EXPECT_EQ(ret, CHILD_OK);
467 }
468 
469 /*
470  * A mount of a mount namespace file is left out of the copy that
471  * unshare(CLONE_NEWNS) makes, together with everything mounted on it, so
472  * it can't take over the lock of a cover it is moved beneath. The child
473  * tries just that with the namespace file of a grandchild and gets EINVAL.
474  * A bind of an ordinary file takes the lock over, and the file is still
475  * covered in the next copy of the mount namespace.
476  */
ns_file_child(const char * base)477 static int ns_file_child(const char *base)
478 {
479 	char secret[PATH_LEN], mine[PATH_LEN], nsfile[PATH_LEN], buf[32];
480 	int ret = CHILD_OK, pfd[2];
481 	pid_t pid;
482 
483 	snprintf(secret, sizeof(secret), "%s/P/secret", base);
484 	snprintf(mine, sizeof(mine), "%s/mine", base);
485 
486 	if (enter_userns() || pipe(pfd))
487 		return CHILD_SETUP;
488 	/* the cover is locked */
489 	if (!umount2(secret, 0) || errno != EINVAL)
490 		return CHILD_STEP1;
491 
492 	/* a mount namespace newer than our own, so that a bind of it is allowed */
493 	pid = fork();
494 	if (pid < 0)
495 		return CHILD_SETUP;
496 	if (pid == 0) {
497 		if (unshare(CLONE_NEWNS) || send_byte(pfd[1]))
498 			_exit(1);
499 		pause();
500 		_exit(0);
501 	}
502 	if (wait_byte(pfd[0]))
503 		return CHILD_SETUP;
504 	snprintf(nsfile, sizeof(nsfile), "/proc/%d/ns/mnt", pid);
505 	if (!move_beneath(nsfile, secret) || errno != EINVAL)
506 		ret = CHILD_STEP2;
507 	kill(pid, SIGKILL);
508 	waitpid(pid, NULL, 0);
509 	if (ret)
510 		return ret;
511 
512 	/* an ordinary file beneath the cover takes the lock over */
513 	if (move_beneath(mine, secret))
514 		return CHILD_STEP3;
515 	if (umount2(secret, 0))
516 		return CHILD_STEP3;
517 	if (!umount2(secret, 0) || errno != EINVAL)
518 		return CHILD_STEP3;
519 	/* and it is in the next copy of the namespace */
520 	if (unshare(CLONE_NEWNS))
521 		return CHILD_SETUP;
522 	if (read_word(secret, buf, sizeof(buf)) || strcmp(buf, "mine"))
523 		return CHILD_STEP4;
524 	return CHILD_OK;
525 }
526 
TEST_F(locked_mount,mount_namespace_file_cant_take_the_lock)527 TEST_F(locked_mount, mount_namespace_file_cant_take_the_lock)
528 {
529 	char p[PATH_LEN], q[PATH_LEN];
530 	pid_t pid;
531 	int ret;
532 
533 	snprintf(p, sizeof(p), "%s/P", self->base);
534 	ASSERT_EQ(mkdir(p, 0755), 0);
535 	ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0);
536 	snprintf(p, sizeof(p), "%s/P/secret", self->base);
537 	ASSERT_EQ(create_file(p, "covered-by-root\n"), 0);
538 	snprintf(q, sizeof(q), "%s/P/empty", self->base);
539 	ASSERT_EQ(create_file(q, ""), 0);
540 	/* the cover: an empty file over the secret */
541 	ASSERT_EQ(mount(q, p, NULL, MS_BIND, NULL), 0);
542 	snprintf(q, sizeof(q), "%s/mine", self->base);
543 	ASSERT_EQ(create_file(q, "mine"), 0);
544 
545 	pid = fork();
546 	ASSERT_GE(pid, 0);
547 	if (pid == 0)
548 		_exit(ns_file_child(self->base));
549 	ret = wait_child(pid);
550 	TH_LOG("child exit code %d", ret);
551 	EXPECT_EQ(ret, CHILD_OK);
552 }
553 
554 TEST_HARNESS_MAIN
555