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 posix_spawn_pipe_np(3C), which spawns "sh -c <cmd>" and
18 * returns a pipe fd for reading from or writing to the child.
19 */
20
21 #include <err.h>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <limits.h>
25 #include <spawn.h>
26 #include <stdbool.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <wait.h>
32 #include <sys/sysmacros.h>
33 #include <sys/debug.h>
34
35 #include "posix_spawn_common.h"
36
37 typedef struct spawn_pipe_test {
38 const char *spt_name;
39 bool (*spt_func)(struct spawn_pipe_test *);
40 } spawn_pipe_test_t;
41
42 /*
43 * Read from a child process: "sh -c echo hello". Verify we get "hello\n".
44 */
45 static bool
read_from_child_test(spawn_pipe_test_t * test)46 read_from_child_test(spawn_pipe_test_t *test)
47 {
48 const char *desc = test->spt_name;
49 posix_spawn_file_actions_t fact;
50 posix_spawnattr_t attr;
51 pid_t pid;
52 int fd, ret;
53 char buf[64];
54 ssize_t n;
55 siginfo_t sig;
56
57 VERIFY0(posix_spawn_file_actions_init(&fact));
58 VERIFY0(posix_spawnattr_init(&attr));
59
60 ret = posix_spawn_pipe_np(&pid, &fd, "echo hello", B_FALSE,
61 &fact, &attr);
62 if (ret != 0) {
63 warnx("TEST FAILED: %s: posix_spawn_pipe_np failed with %s",
64 desc, strerrorname_np(ret));
65 VERIFY0(posix_spawn_file_actions_destroy(&fact));
66 VERIFY0(posix_spawnattr_destroy(&attr));
67 return (false);
68 }
69
70 n = read(fd, buf, sizeof (buf) - 1);
71 (void) close(fd);
72
73 if (waitid(P_PID, pid, &sig, WEXITED) != 0)
74 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: waitid", desc);
75
76 if (n < 0) {
77 warn("TEST FAILED: %s: read from pipe failed", desc);
78 VERIFY0(posix_spawn_file_actions_destroy(&fact));
79 VERIFY0(posix_spawnattr_destroy(&attr));
80 return (false);
81 }
82
83 buf[n] = '\0';
84
85 if (strcmp(buf, "hello\n") != 0) {
86 warnx("TEST FAILED: %s: expected 'hello\\n', got '%s'",
87 desc, buf);
88 VERIFY0(posix_spawn_file_actions_destroy(&fact));
89 VERIFY0(posix_spawnattr_destroy(&attr));
90 return (false);
91 }
92
93 VERIFY0(posix_spawn_file_actions_destroy(&fact));
94 VERIFY0(posix_spawnattr_destroy(&attr));
95
96 return (sig.si_code == CLD_EXITED && sig.si_status == 0);
97 }
98
99 /*
100 * Write to a child process: pipe data to "cat", capture its output via a
101 * temporary file. Verify the data round-trips.
102 */
103 static bool
write_to_child_test(spawn_pipe_test_t * test)104 write_to_child_test(spawn_pipe_test_t *test)
105 {
106 const char *desc = test->spt_name;
107 posix_spawn_file_actions_t fact;
108 posix_spawnattr_t attr;
109 pid_t pid;
110 int fd, ret;
111 siginfo_t sig;
112 bool bret = true;
113 char tmpfile[] = "/tmp/posix_spawn_pipe_np.XXXXXX";
114 char cmd[PATH_MAX];
115 int tmpfd;
116 char buf[64];
117 ssize_t n;
118 const char *msg = "test data\n";
119
120 tmpfd = mkstemp(tmpfile);
121 if (tmpfd == -1) {
122 warn("TEST FAILED: %s: mkstemp failed", desc);
123 return (false);
124 }
125 (void) close(tmpfd);
126
127 (void) snprintf(cmd, sizeof (cmd), "cat > %s", tmpfile);
128
129 VERIFY0(posix_spawn_file_actions_init(&fact));
130 VERIFY0(posix_spawnattr_init(&attr));
131
132 ret = posix_spawn_pipe_np(&pid, &fd, cmd, B_TRUE, &fact, &attr);
133 if (ret != 0) {
134 warnx("TEST FAILED: %s: posix_spawn_pipe_np failed with %s",
135 desc, strerrorname_np(ret));
136 bret = false;
137 goto out;
138 }
139
140 if (write(fd, msg, strlen(msg)) != (ssize_t)strlen(msg)) {
141 warn("TEST FAILED: %s: write to pipe failed", desc);
142 bret = false;
143 }
144 (void) close(fd);
145
146 if (waitid(P_PID, pid, &sig, WEXITED) != 0)
147 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: waitid", desc);
148
149 if (sig.si_code != CLD_EXITED || sig.si_status != 0) {
150 warnx("TEST FAILED: %s: child did not exit cleanly", desc);
151 bret = false;
152 goto out;
153 }
154
155 /* Read back from the temporary file and verify. */
156 tmpfd = open(tmpfile, O_RDONLY);
157 if (tmpfd == -1) {
158 warn("TEST FAILED: %s: open(%s) failed", desc, tmpfile);
159 bret = false;
160 goto out;
161 }
162
163 n = read(tmpfd, buf, sizeof (buf) - 1);
164 (void) close(tmpfd);
165
166 if (n < 0) {
167 warn("TEST FAILED: %s: read from tmpfile failed", desc);
168 bret = false;
169 goto out;
170 }
171
172 buf[n] = '\0';
173 if (strcmp(buf, msg) != 0) {
174 warnx("TEST FAILED: %s: expected '%s', got '%s'",
175 desc, msg, buf);
176 bret = false;
177 }
178
179 out:
180 (void) unlink(tmpfile);
181 VERIFY0(posix_spawn_file_actions_destroy(&fact));
182 VERIFY0(posix_spawnattr_destroy(&attr));
183
184 return (bret);
185 }
186
187 /*
188 * Error case: spawn a command that doesn't exist.
189 */
190 static bool
bad_cmd_test(spawn_pipe_test_t * test)191 bad_cmd_test(spawn_pipe_test_t *test)
192 {
193 const char *desc = test->spt_name;
194 posix_spawn_file_actions_t fact;
195 posix_spawnattr_t attr;
196 pid_t pid;
197 int fd, ret;
198 siginfo_t sig;
199
200 VERIFY0(posix_spawn_file_actions_init(&fact));
201 VERIFY0(posix_spawnattr_init(&attr));
202
203 /*
204 * posix_spawn_pipe_np spawns "sh -c <cmd>", so even a bad command
205 * will successfully spawn sh. The shell itself will report the error
206 * via a non-zero exit status.
207 */
208 ret = posix_spawn_pipe_np(&pid, &fd, "/nonexistent/cmd", B_FALSE,
209 &fact, &attr);
210 if (ret != 0) {
211 warnx("TEST FAILED: %s: posix_spawn_pipe_np failed with %s, "
212 "expected success (sh spawns)", desc, strerrorname_np(ret));
213 VERIFY0(posix_spawn_file_actions_destroy(&fact));
214 VERIFY0(posix_spawnattr_destroy(&attr));
215 return (false);
216 }
217
218 (void) close(fd);
219
220 if (waitid(P_PID, pid, &sig, WEXITED) != 0)
221 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: waitid", desc);
222
223 VERIFY0(posix_spawn_file_actions_destroy(&fact));
224 VERIFY0(posix_spawnattr_destroy(&attr));
225
226 if (sig.si_code != CLD_EXITED || sig.si_status == 0) {
227 warnx("TEST FAILED: %s: expected non-zero exit from shell",
228 desc);
229 return (false);
230 }
231
232 return (true);
233 }
234
235 static spawn_pipe_test_t tests[] = {
236 { .spt_name = "pipe_np: read from child (echo)",
237 .spt_func = read_from_child_test },
238 { .spt_name = "pipe_np: write to child (cat)",
239 .spt_func = write_to_child_test },
240 { .spt_name = "pipe_np: bad command exits non-zero",
241 .spt_func = bad_cmd_test },
242 };
243
244 int
main(void)245 main(void)
246 {
247 int ret = EXIT_SUCCESS;
248
249 for (size_t i = 0; i < ARRAY_SIZE(tests); i++) {
250 if (tests[i].spt_func(&tests[i]))
251 (void) printf("TEST PASSED: %s\n", tests[i].spt_name);
252 else
253 ret = EXIT_FAILURE;
254 }
255
256 if (ret == EXIT_SUCCESS)
257 (void) printf("All tests passed successfully!\n");
258
259 return (ret);
260 }
261