xref: /linux/tools/testing/selftests/landlock/trace.h (revision 7199989f3f3194d653b024ce8e79cea6b15e38b9)
1 /* SPDX-License-Identifier: GPL-2.0 */
2 /*
3  * Landlock trace test helpers
4  *
5  * Copyright © 2026 Cloudflare, Inc.
6  */
7 
8 #define _GNU_SOURCE
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <regex.h>
12 #include <stdbool.h>
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <sys/mount.h>
17 #include <sys/stat.h>
18 #include <unistd.h>
19 
20 #include "kselftest_harness.h"
21 
22 #define TRACEFS_ROOT "/sys/kernel/tracing"
23 #define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
24 #define TRACEFS_CREATE_RULESET_ENABLE \
25 	TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
26 #define TRACEFS_CREATE_DOMAIN_ENABLE \
27 	TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
28 #define TRACEFS_ENFORCE_DOMAIN_ENABLE \
29 	TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable"
30 #define TRACEFS_ADD_RULE_FS_ENABLE \
31 	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
32 #define TRACEFS_ADD_RULE_NET_ENABLE \
33 	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
34 #define TRACEFS_CHECK_RULE_FS_ENABLE \
35 	TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
36 #define TRACEFS_CHECK_RULE_NET_ENABLE \
37 	TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
38 #define TRACEFS_DENY_ACCESS_FS_ENABLE \
39 	TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
40 #define TRACEFS_DENY_ACCESS_NET_ENABLE \
41 	TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
42 #define TRACEFS_DENY_PTRACE_ENABLE \
43 	TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
44 #define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
45 	TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
46 #define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
47 	TRACEFS_LANDLOCK_DIR                           \
48 	"/landlock_deny_scope_abstract_unix_socket/enable"
49 #define TRACEFS_FREE_DOMAIN_ENABLE \
50 	TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
51 #define TRACEFS_FREE_RULESET_ENABLE \
52 	TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
53 #define TRACEFS_TRACE TRACEFS_ROOT "/trace"
54 #define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
55 #define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
56 
57 #define TRACE_BUFFER_SIZE (64 * 1024)
58 
59 /*
60  * Trace line prefix: matches the ftrace "trace" file format.  Format: "
61  * <task>-<pid> [<cpu>] <flags> <timestamp>: "
62  *
63  * The task parameter must be a string literal truncated to 15 chars
64  * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm.  The
65  * pattern accepts either the expected task name or "<...>" because the ftrace
66  * comm cache may evict short-lived processes (e.g., forked children that exit
67  * before the trace buffer is read).
68  *
69  * No unescaped '.' in any REGEX macro; literal dots use '\\.'.
70  */
71 #define TRACE_PREFIX(task)  \
72 	"^ *\\(<\\.\\.\\.>" \
73 	"\\|" task "\\)"    \
74 	"-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
75 
76 /*
77  * Task name for events emitted by kworker threads (e.g., free_domain fires from
78  * a work queue, not from the test process).
79  */
80 #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
81 
82 #define REGEX_ADD_RULE_FS(task)           \
83 	TRACE_PREFIX(task)                \
84 	"landlock_add_rule_fs: "          \
85 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
86 	"access_rights=[a-z_|]* "         \
87 	"dev=[0-9]\\+:[0-9]\\+ "          \
88 	"ino=[0-9]\\+ "                   \
89 	"path=[^ ]\\+$"
90 
91 #define REGEX_ADD_RULE_NET(task)          \
92 	TRACE_PREFIX(task)                \
93 	"landlock_add_rule_net: "         \
94 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
95 	"access_rights=[a-z_|]* "         \
96 	"port=[0-9]\\+$"
97 
98 #define REGEX_CREATE_RULESET(task)        \
99 	TRACE_PREFIX(task)                \
100 	"landlock_create_ruleset: "       \
101 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
102 	"handled_fs=[a-z_|]* "            \
103 	"handled_net=[a-z_|]* "           \
104 	"scoped=[a-z_|]*$"
105 
106 #define REGEX_CREATE_DOMAIN(task)  \
107 	TRACE_PREFIX(task)         \
108 	"landlock_create_domain: " \
109 	"domain=[0-9a-f]\\+ "      \
110 	"parent=[0-9a-f]\\+ "      \
111 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
112 
113 #define REGEX_CHECK_RULE_FS(task)  \
114 	TRACE_PREFIX(task)         \
115 	"landlock_check_rule_fs: " \
116 	"domain=[0-9a-f]\\+ "      \
117 	"access_request=[a-z_|]* " \
118 	"dev=[0-9]\\+:[0-9]\\+ "   \
119 	"ino=[0-9]\\+ "            \
120 	"grants={[a-z_|,]*}$"
121 
122 #define REGEX_CHECK_RULE_NET(task)  \
123 	TRACE_PREFIX(task)          \
124 	"landlock_check_rule_net: " \
125 	"domain=[0-9a-f]\\+ "       \
126 	"access_request=[a-z_|]* "  \
127 	"port=[0-9]\\+ "            \
128 	"grants={[a-z_|,]*}$"
129 
130 #define REGEX_DENY_ACCESS_FS(task)  \
131 	TRACE_PREFIX(task)          \
132 	"landlock_deny_access_fs: " \
133 	"domain=[0-9a-f]\\+ "       \
134 	"same_exec=[01] "           \
135 	"logged=[01] "              \
136 	"blockers=[a-z_|]* "        \
137 	"dev=[0-9]\\+:[0-9]\\+ "    \
138 	"ino=[0-9]\\+ "             \
139 	"path=[^ ]*$"
140 
141 #define REGEX_DENY_ACCESS_NET(task)  \
142 	TRACE_PREFIX(task)           \
143 	"landlock_deny_access_net: " \
144 	"domain=[0-9a-f]\\+ "        \
145 	"same_exec=[01] "            \
146 	"logged=[01] "               \
147 	"blockers=[a-z_|]* "         \
148 	"sport=[0-9]\\+ "            \
149 	"dport=[0-9]\\+$"
150 
151 #define REGEX_DENY_PTRACE(task)      \
152 	TRACE_PREFIX(task)           \
153 	"landlock_deny_ptrace: "     \
154 	"domain=[0-9a-f]\\+ "        \
155 	"same_exec=[01] "            \
156 	"logged=[01] "               \
157 	"tracee_domain=[0-9a-f]\\+ " \
158 	"tracee_pid=[0-9]\\+ "       \
159 	"tracee_comm=[^ ]*$"
160 
161 #define REGEX_DENY_SCOPE_SIGNAL(task)  \
162 	TRACE_PREFIX(task)             \
163 	"landlock_deny_scope_signal: " \
164 	"domain=[0-9a-f]\\+ "          \
165 	"same_exec=[01] "              \
166 	"logged=[01] "                 \
167 	"target_domain=[0-9a-f]\\+ "   \
168 	"target_pid=[0-9]\\+ "         \
169 	"target_comm=[^ ]*$"
170 
171 #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task)  \
172 	TRACE_PREFIX(task)                           \
173 	"landlock_deny_scope_abstract_unix_socket: " \
174 	"domain=[0-9a-f]\\+ "                        \
175 	"same_exec=[01] "                            \
176 	"logged=[01] "                               \
177 	"peer_domain=[0-9a-f]\\+ "                   \
178 	"peer_pid=[0-9]\\+ "                         \
179 	"sun_path=[^ ]*$"
180 
181 #define REGEX_FREE_DOMAIN(task)  \
182 	TRACE_PREFIX(task)       \
183 	"landlock_free_domain: " \
184 	"domain=[0-9a-f]\\+ "    \
185 	"denials=[0-9]\\+$"
186 
187 #define REGEX_FREE_RULESET(task)  \
188 	TRACE_PREFIX(task)        \
189 	"landlock_free_ruleset: " \
190 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
191 
192 static int __maybe_unused tracefs_write(const char *path, const char *value)
193 {
194 	int fd;
195 	ssize_t ret;
196 	size_t len = strlen(value);
197 
198 	fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
199 	if (fd < 0)
200 		return -errno;
201 
202 	ret = write(fd, value, len);
203 	close(fd);
204 	if (ret < 0)
205 		return -errno;
206 	if ((size_t)ret != len)
207 		return -EIO;
208 
209 	return 0;
210 }
211 
212 static int __maybe_unused tracefs_write_int(const char *path, int value)
213 {
214 	char buf[32];
215 
216 	snprintf(buf, sizeof(buf), "%d", value);
217 	return tracefs_write(path, buf);
218 }
219 
220 static int __maybe_unused tracefs_setup(void)
221 {
222 	struct stat st;
223 
224 	/* Mount tracefs if not already mounted. */
225 	if (stat(TRACEFS_ROOT, &st) != 0) {
226 		int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
227 
228 		if (ret)
229 			return -errno;
230 	}
231 
232 	/* Verify landlock events are available. */
233 	if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
234 		return -ENOENT;
235 
236 	return 0;
237 }
238 
239 /*
240  * Set up PID-based event filtering so only events from the current process and
241  * its children are recorded.  This is analogous to audit's AUDIT_EXE filter: it
242  * prevents events from unrelated processes from polluting the trace buffer.
243  */
244 static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
245 {
246 	int ret;
247 
248 	/* Enable event-fork so children inherit the PID filter. */
249 	ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
250 	if (ret)
251 		return ret;
252 
253 	return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
254 }
255 
256 /* Clear the PID filter to stop filtering by PID. */
257 static int __maybe_unused tracefs_clear_pid_filter(void)
258 {
259 	return tracefs_write(TRACEFS_SET_EVENT_PID, "");
260 }
261 
262 static int __maybe_unused tracefs_enable_event(const char *enable_path,
263 					       bool enable)
264 {
265 	return tracefs_write(enable_path, enable ? "1" : "0");
266 }
267 
268 static int __maybe_unused tracefs_clear(void)
269 {
270 	return tracefs_write(TRACEFS_TRACE, "");
271 }
272 
273 /*
274  * Reads the trace buffer content into a newly allocated buffer.  The caller is
275  * responsible for freeing the returned buffer.  Returns NULL on error.
276  */
277 static char __maybe_unused *tracefs_read_trace(void)
278 {
279 	char *buf;
280 	int fd;
281 	ssize_t total = 0, ret;
282 
283 	buf = malloc(TRACE_BUFFER_SIZE);
284 	if (!buf)
285 		return NULL;
286 
287 	fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
288 	if (fd < 0) {
289 		free(buf);
290 		return NULL;
291 	}
292 
293 	while (total < TRACE_BUFFER_SIZE - 1) {
294 		ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
295 		if (ret <= 0)
296 			break;
297 		total += ret;
298 	}
299 	close(fd);
300 	buf[total] = '\0';
301 	return buf;
302 }
303 
304 /* Counts the number of lines in @buf matching the basic regex @pattern. */
305 static int __maybe_unused tracefs_count_matches(const char *buf,
306 						const char *pattern)
307 {
308 	regex_t regex;
309 	int count = 0;
310 	const char *line, *end;
311 
312 	if (regcomp(&regex, pattern, 0) != 0)
313 		return -EINVAL;
314 
315 	line = buf;
316 	while (*line) {
317 		end = strchr(line, '\n');
318 		if (!end)
319 			end = line + strlen(line);
320 
321 		/* Create a temporary NUL-terminated line. */
322 		size_t len = end - line;
323 		char *tmp = malloc(len + 1);
324 
325 		if (tmp) {
326 			memcpy(tmp, line, len);
327 			tmp[len] = '\0';
328 			if (regexec(&regex, tmp, 0, NULL, 0) == 0)
329 				count++;
330 			free(tmp);
331 		}
332 
333 		if (*end == '\n')
334 			line = end + 1;
335 		else
336 			break;
337 	}
338 
339 	regfree(&regex);
340 	return count;
341 }
342 
343 /*
344  * Extracts the value of a named field from a trace line in @buf.  Searches for
345  * the first line matching @line_pattern, then extracts the value after
346  * "@field_name=" into @out.  Stops at space or newline.
347  *
348  * Returns 0 on success, -ENOENT if no match.
349  */
350 static int __maybe_unused tracefs_extract_field(const char *buf,
351 						const char *line_pattern,
352 						const char *field_name,
353 						char *out, size_t out_size)
354 {
355 	regex_t regex;
356 	const char *line, *end;
357 
358 	if (regcomp(&regex, line_pattern, 0) != 0)
359 		return -EINVAL;
360 
361 	line = buf;
362 	while (*line) {
363 		end = strchr(line, '\n');
364 		if (!end)
365 			end = line + strlen(line);
366 
367 		size_t len = end - line;
368 		char *tmp = malloc(len + 1);
369 
370 		if (tmp) {
371 			const char *field, *val_start;
372 			size_t field_len, val_len;
373 
374 			memcpy(tmp, line, len);
375 			tmp[len] = '\0';
376 
377 			if (regexec(&regex, tmp, 0, NULL, 0) != 0) {
378 				free(tmp);
379 				goto next;
380 			}
381 
382 			/*
383 			 * Find "field_name=" in the line, ensuring a word
384 			 * boundary before the field name to avoid substring
385 			 * matches (e.g., "port" in "sport").
386 			 */
387 			field_len = strlen(field_name);
388 			field = tmp;
389 			while ((field = strstr(field, field_name))) {
390 				if (field[field_len] == '=' &&
391 				    (field == tmp || field[-1] == ' '))
392 					break;
393 				field++;
394 			}
395 			if (!field) {
396 				free(tmp);
397 				regfree(&regex);
398 				return -ENOENT;
399 			}
400 
401 			val_start = field + field_len + 1;
402 			val_len = 0;
403 			while (val_start[val_len] &&
404 			       val_start[val_len] != ' ' &&
405 			       val_start[val_len] != '\n')
406 				val_len++;
407 
408 			if (val_len >= out_size)
409 				val_len = out_size - 1;
410 			memcpy(out, val_start, val_len);
411 			out[val_len] = '\0';
412 
413 			free(tmp);
414 			regfree(&regex);
415 			return 0;
416 		}
417 next:
418 		if (*end == '\n')
419 			line = end + 1;
420 		else
421 			break;
422 	}
423 
424 	regfree(&regex);
425 	return -ENOENT;
426 }
427 
428 /*
429  * Common fixture setup for trace tests.  Mounts tracefs if needed and sets a
430  * PID filter.  The caller must create a mount namespace first
431  * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
432  * mount; the trace buffer, per-event enable flags, and PID filter are global
433  * kernel state, scoped to the test by the PID filter.
434  *
435  * Returns 0 on success, -errno on failure (caller should SKIP).
436  */
437 static int __maybe_unused tracefs_fixture_setup(void)
438 {
439 	int ret;
440 
441 	ret = tracefs_setup();
442 	if (ret)
443 		return ret;
444 
445 	return tracefs_set_pid_filter(getpid());
446 }
447 
448 static void __maybe_unused tracefs_fixture_teardown(void)
449 {
450 	tracefs_clear_pid_filter();
451 }
452 
453 /*
454  * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
455  * drops the capability.  Returns the value from @func, or -EPERM if the
456  * capability manipulation fails.
457  */
458 static int __maybe_unused tracefs_priv_call(int (*func)(void))
459 {
460 	const cap_value_t admin = CAP_SYS_ADMIN;
461 	cap_t cap_p;
462 	int ret;
463 
464 	cap_p = cap_get_proc();
465 	if (!cap_p)
466 		return -EPERM;
467 
468 	if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
469 	    cap_set_proc(cap_p)) {
470 		cap_free(cap_p);
471 		return -EPERM;
472 	}
473 
474 	ret = func();
475 
476 	cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
477 	cap_set_proc(cap_p);
478 	cap_free(cap_p);
479 	return ret;
480 }
481 
482 /* Read the trace buffer with elevated privileges.  Returns NULL on failure. */
483 static char __maybe_unused *tracefs_read_buf(void)
484 {
485 	/* Cannot use tracefs_priv_call() because the return type is char *. */
486 	cap_t cap_p;
487 	char *buf;
488 	const cap_value_t admin = CAP_SYS_ADMIN;
489 
490 	cap_p = cap_get_proc();
491 	if (!cap_p)
492 		return NULL;
493 
494 	if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
495 	    cap_set_proc(cap_p)) {
496 		cap_free(cap_p);
497 		return NULL;
498 	}
499 
500 	buf = tracefs_read_trace();
501 
502 	cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
503 	cap_set_proc(cap_p);
504 	cap_free(cap_p);
505 	return buf;
506 }
507 
508 /* Clear the trace buffer with elevated privileges.  Returns 0 on success. */
509 static int __maybe_unused tracefs_clear_buf(void)
510 {
511 	return tracefs_priv_call(tracefs_clear);
512 }
513 
514 /*
515  * Forks a child that creates a Landlock sandbox and performs an FS access.  The
516  * parent waits for the child, then reads the trace buffer.
517  *
518  * Requires common.h and wrappers.h to be included before trace.h.
519  */
520 static void __maybe_unused sandbox_child_fs_access(
521 	struct __test_metadata *const _metadata, const char *rule_path,
522 	__u64 handled_access, __u64 allowed_access, const char *access_path)
523 {
524 	pid_t pid;
525 	int status;
526 
527 	pid = fork();
528 	ASSERT_LE(0, pid);
529 
530 	if (pid == 0) {
531 		struct landlock_ruleset_attr ruleset_attr = {
532 			.handled_access_fs = handled_access,
533 		};
534 		struct landlock_path_beneath_attr path_beneath = {
535 			.allowed_access = allowed_access,
536 		};
537 		int ruleset_fd, fd;
538 
539 		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
540 						     sizeof(ruleset_attr), 0);
541 		if (ruleset_fd < 0)
542 			_exit(1);
543 
544 		path_beneath.parent_fd =
545 			open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
546 		if (path_beneath.parent_fd < 0) {
547 			close(ruleset_fd);
548 			_exit(1);
549 		}
550 
551 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
552 				      &path_beneath, 0)) {
553 			close(path_beneath.parent_fd);
554 			close(ruleset_fd);
555 			_exit(1);
556 		}
557 		close(path_beneath.parent_fd);
558 
559 		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
560 		if (landlock_restrict_self(ruleset_fd, 0)) {
561 			close(ruleset_fd);
562 			_exit(1);
563 		}
564 		close(ruleset_fd);
565 
566 		fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
567 		if (fd >= 0)
568 			close(fd);
569 
570 		_exit(0);
571 	}
572 
573 	ASSERT_EQ(pid, waitpid(pid, &status, 0));
574 	ASSERT_TRUE(WIFEXITED(status));
575 	EXPECT_EQ(0, WEXITSTATUS(status));
576 }
577 
578 /*
579  * Forks a child that creates a Landlock sandbox allowing execute+read_dir for
580  * /usr and execute-only for ".", then execs ./true.  The true binary opens "."
581  * on startup, triggering a read_dir denial with same_exec=0.  The parent waits
582  * for the child to exit.
583  */
584 static void __maybe_unused sandbox_child_exec_true(
585 	struct __test_metadata *const _metadata, __u32 restrict_flags)
586 {
587 	pid_t pid;
588 	int status;
589 
590 	pid = fork();
591 	ASSERT_LE(0, pid);
592 
593 	if (pid == 0) {
594 		struct landlock_ruleset_attr attr = {
595 			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
596 					     LANDLOCK_ACCESS_FS_EXECUTE,
597 		};
598 		struct landlock_path_beneath_attr path_beneath = {
599 			.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
600 					  LANDLOCK_ACCESS_FS_READ_DIR,
601 		};
602 		int ruleset_fd;
603 
604 		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
605 		if (ruleset_fd < 0)
606 			_exit(1);
607 
608 		path_beneath.parent_fd =
609 			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
610 		if (path_beneath.parent_fd >= 0) {
611 			landlock_add_rule(ruleset_fd,
612 					  LANDLOCK_RULE_PATH_BENEATH,
613 					  &path_beneath, 0);
614 			close(path_beneath.parent_fd);
615 		}
616 
617 		path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
618 		path_beneath.parent_fd =
619 			open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
620 		if (path_beneath.parent_fd >= 0) {
621 			landlock_add_rule(ruleset_fd,
622 					  LANDLOCK_RULE_PATH_BENEATH,
623 					  &path_beneath, 0);
624 			close(path_beneath.parent_fd);
625 		}
626 
627 		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
628 		if (landlock_restrict_self(ruleset_fd, restrict_flags))
629 			_exit(1);
630 		close(ruleset_fd);
631 
632 		execl("./true", "./true", NULL);
633 		_exit(1);
634 	}
635 
636 	ASSERT_EQ(pid, waitpid(pid, &status, 0));
637 	ASSERT_TRUE(WIFEXITED(status));
638 	EXPECT_EQ(0, WEXITSTATUS(status));
639 }
640