The Pedigree Project 0.1
testsuite/exec.c
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include <errno.h>
9#include <fcntl.h>
10#include <limits.h>
11#include <stdio.h>
12#include <stdlib.h>
13#include <string.h>
14#include <unistd.h>
15
16#include <sys/stat.h>
17#include <sys/types.h>
18#include <sys/wait.h>
19
20extern void fail(void) __attribute__((noreturn));
21extern char** environ;
22
23int exec_arguments_child(int argc, char* argv[]) {
24 const char* arguments[] = {"", "--exec-arguments-child", "", "before", "", "after", ""};
25 const char* environment[] = {"",
26 "PEDIGREE_EXEC_BEFORE=one",
27 "",
28 "PEDIGREE_EXEC_AFTER=two",
29 "PEDIGREE_EXEC_DUP=first",
30 "PEDIGREE_EXEC_DUP=second",
31 ""};
32 if (argc != (int)(sizeof(arguments) / sizeof(arguments[0]))) {
33 return 124;
34 }
35 for (size_t i = 0; i < sizeof(arguments) / sizeof(arguments[0]); ++i) {
36 if (strcmp(argv[i], arguments[i])) {
37 return 125;
38 }
39 }
40 for (size_t i = 0; i < sizeof(environment) / sizeof(environment[0]); ++i) {
41 if (!environ[i] || strcmp(environ[i], environment[i])) {
42 printf("exec environment entry %lu changed\n", (unsigned long)i);
43 return 126;
44 }
45 }
46 if (environ[sizeof(environment) / sizeof(environment[0])]) {
47 return 127;
48 }
49 const char* duplicate = getenv("PEDIGREE_EXEC_DUP");
50 if (!duplicate || strcmp(duplicate, "first")) {
51 return 133;
52 }
53
54 const char commandLine[] = "\0--exec-arguments-child\0\0before\0\0after\0\0";
55 // Exercise whole fields and reads starting at each individual NUL separator.
56 const size_t chunks[] = {1, sizeof(commandLine)};
57 for (size_t pass = 0; pass < sizeof(chunks) / sizeof(chunks[0]); ++pass) {
58 const size_t chunk = chunks[pass];
59 int fd = open("/proc/self/cmdline", O_RDONLY);
60 if (fd < 0) {
61 return 128;
62 }
63 char contents[sizeof(commandLine)];
64 size_t length = 0;
65 for (;;) {
66 ssize_t result = read(fd, contents + length,
67 chunk < sizeof(contents) - length ? chunk : sizeof(contents) - length);
68 if (result < 0 && errno == EINTR) {
69 continue;
70 }
71 if (result < 0) {
72 close(fd);
73 return 129;
74 }
75 if (!result) {
76 break;
77 }
78 length += (size_t)result;
79 if (length == sizeof(contents)) {
80 close(fd);
81 return 130;
82 }
83 }
84 if (close(fd) || length != sizeof(commandLine) - 1 || memcmp(contents, commandLine, length)) {
85 return 131;
86 }
87 }
88 return 0;
89}
90
91void test_exec_arguments(const char* program) {
92 puts("Testing empty exec arguments, environment, and proc cmdline...");
93 fflush(stdout);
94 pid_t child = fork();
95 if (child < 0) {
96 fail();
97 }
98 if (!child) {
99 char* const arguments[] = {(char*)"", (char*)"--exec-arguments-child",
100 (char*)"", (char*)"before",
101 (char*)"", (char*)"after",
102 (char*)"", 0};
103 char* const environment[] = {(char*)"",
104 (char*)"PEDIGREE_EXEC_BEFORE=one",
105 (char*)"",
106 (char*)"PEDIGREE_EXEC_AFTER=two",
107 (char*)"PEDIGREE_EXEC_DUP=first",
108 (char*)"PEDIGREE_EXEC_DUP=second",
109 (char*)"",
110 0};
111 execve(program, arguments, environment);
112 _exit(132);
113 }
114 int statusCode = 0;
115 pid_t waited;
116 do {
117 waited = waitpid(child, &statusCode, 0);
118 } while (waited < 0 && errno == EINTR);
119 if (waited != child || !WIFEXITED(statusCode) || WEXITSTATUS(statusCode)) {
120 printf("empty exec child status=%d\n", statusCode);
121 fail();
122 }
123 puts("OK");
124 fflush(stdout);
125}
126
127static int write_fixture(const char* path, const char* contents, size_t length, mode_t mode) {
128 int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, mode);
129 if (fd < 0)
130 return -1;
131
132 size_t written = 0;
133 while (written < length) {
134 ssize_t result = write(fd, contents + written, length - written);
135 if (result < 0 && errno == EINTR)
136 continue;
137 if (result <= 0) {
138 close(fd);
139 unlink(path);
140 return -1;
141 }
142 written += (size_t)result;
143 }
144
145 if (close(fd) || chmod(path, mode)) {
146 unlink(path);
147 return -1;
148 }
149 return 0;
150}
151
152static int write_script(const char* path, const char* shebang, mode_t mode) {
153 return write_fixture(path, shebang, strlen(shebang), mode);
154}
155
156int exec_shebang_child(int argc, char* argv[]) {
157 if (!strcmp(argv[1], "--exec-shebang-unexpected-child"))
158 return 120;
159
160 if (!strcmp(argv[1], "--exec-shebang-child beta\tgamma")) {
161 return !(argc == 10 && !strcmp(argv[0], argv[5]) && !strcmp(argv[2], argv[6]) &&
162 !strcmp(argv[3], "alpha one") && !strcmp(argv[4], argv[7]) &&
163 !strcmp(argv[8], "tail-one") && !strcmp(argv[9], "tail two"));
164 }
165
166 if (!strcmp(argv[1], "--exec-shebang-depth-child")) {
167 return !(argc == 12 && !strcmp(argv[0], argv[6]) && !strcmp(argv[2], argv[7]) &&
168 !strcmp(argv[3], argv[8]) && !strcmp(argv[4], argv[9]) && !strcmp(argv[5], argv[10]) &&
169 !strcmp(argv[11], "depth-tail"));
170 }
171
172 return 121;
173}
174
175static int exec_returns_errno(const char* path, int expected) {
176 pid_t child = fork();
177 if (child < 0)
178 return 0;
179 if (!child) {
180 char* const arguments[] = {(char*)"wrong-shebang-argv-zero", (char*)"tail", 0};
181 errno = 0;
182 execv(path, arguments);
183 _exit(errno == expected ? 0 : 122);
184 }
185
186 int statusCode = 0;
187 return waitpid(child, &statusCode, 0) == child && WIFEXITED(statusCode) &&
188 !WEXITSTATUS(statusCode);
189}
190
191static int exec_succeeds(const char* path, char* const arguments[]) {
192 pid_t child = fork();
193 if (child < 0)
194 return 0;
195 if (!child) {
196 execv(path, arguments);
197 _exit(123);
198 }
199
200 int statusCode = 0;
201 return waitpid(child, &statusCode, 0) == child && WIFEXITED(statusCode) &&
202 !WEXITSTATUS(statusCode);
203}
204
205void test_exec_shebang(const char* program) {
206 puts("Testing bounded shebang execution...");
207 fflush(stdout);
208
209 char permissionScript[PATH_MAX];
210 char permissionOuter[PATH_MAX];
211 char permissionInterpreter[PATH_MAX];
212 char nestedOuter[PATH_MAX];
213 char nestedInterpreter[PATH_MAX];
214 char depthA[PATH_MAX];
215 char depthB[PATH_MAX];
216 char depthC[PATH_MAX];
217 char depthD[PATH_MAX];
218 char depthE[PATH_MAX];
219 char truncated[PATH_MAX];
220 char unterminated[PATH_MAX];
221 char malformed[PATH_MAX];
222 const int processId = getpid();
223 snprintf(permissionScript, sizeof(permissionScript), "/tmp/exec-shebang-permission-%d",
224 processId);
225 snprintf(permissionOuter, sizeof(permissionOuter), "/tmp/exec-shebang-permission-outer-%d",
226 processId);
227 snprintf(permissionInterpreter, sizeof(permissionInterpreter),
228 "/tmp/exec-shebang-permission-interpreter-%d", processId);
229 snprintf(nestedOuter, sizeof(nestedOuter), "/tmp/exec-shebang-nested-outer-%d", processId);
230 snprintf(nestedInterpreter, sizeof(nestedInterpreter), "/tmp/exec-shebang-nested-interpreter-%d",
231 processId);
232 snprintf(depthA, sizeof(depthA), "/tmp/exec-shebang-depth-a-%d", processId);
233 snprintf(depthB, sizeof(depthB), "/tmp/exec-shebang-depth-b-%d", processId);
234 snprintf(depthC, sizeof(depthC), "/tmp/exec-shebang-depth-c-%d", processId);
235 snprintf(depthD, sizeof(depthD), "/tmp/exec-shebang-depth-d-%d", processId);
236 snprintf(depthE, sizeof(depthE), "/tmp/exec-shebang-depth-e-%d", processId);
237 snprintf(truncated, sizeof(truncated), "/tmp/exec-shebang-truncated-%d", processId);
238 snprintf(unterminated, sizeof(unterminated), "/tmp/exec-shebang-unterminated-%d", processId);
239 snprintf(malformed, sizeof(malformed), "/tmp/exec-shebang-malformed-%d", processId);
240
241 const char* fixtures[] = {permissionScript,
242 permissionOuter,
243 permissionInterpreter,
244 nestedOuter,
245 nestedInterpreter,
246 depthA,
247 depthB,
248 depthC,
249 depthD,
250 depthE,
251 truncated,
252 unterminated,
253 malformed};
254 const size_t fixtureCount = sizeof(fixtures) / sizeof(fixtures[0]);
255 for (size_t i = 0; i < fixtureCount; ++i)
256 unlink(fixtures[i]);
257
258 int failed = 0;
259 char line[PATH_MAX + 128];
260 snprintf(line, sizeof(line), "#!%s\t--exec-shebang-unexpected-child\n", program);
261 failed |=
262 write_script(permissionScript, line, 0600) || !exec_returns_errno(permissionScript, EACCES);
263
264 failed |= write_script(permissionInterpreter, line, 0600);
265 snprintf(line, sizeof(line), "#!%s\n", permissionInterpreter);
266 failed |=
267 write_script(permissionOuter, line, 0700) || !exec_returns_errno(permissionOuter, EACCES);
268
269 snprintf(line, sizeof(line), "#!\t%s\t--exec-shebang-child beta\tgamma \t\n", program);
270 failed |= write_script(nestedInterpreter, line, 0700);
271 snprintf(line, sizeof(line), "#!\t%s\talpha one \t \n", nestedInterpreter);
272 failed |= write_script(nestedOuter, line, 0700);
273 char* nestedArguments[] = {(char*)"wrong-shebang-argv-zero",
274 (char*)program,
275 nestedInterpreter,
276 nestedOuter,
277 (char*)"tail-one",
278 (char*)"tail two",
279 0};
280 if (!failed)
281 failed |= !exec_succeeds(nestedOuter, nestedArguments);
282
283 snprintf(line, sizeof(line), "#!%s\t--exec-shebang-depth-child \t\n", program);
284 failed |= write_script(depthD, line, 0700);
285 snprintf(line, sizeof(line), "#!%s\n", depthD);
286 failed |= write_script(depthC, line, 0700);
287 snprintf(line, sizeof(line), "#!%s\n", depthC);
288 failed |= write_script(depthB, line, 0700);
289 snprintf(line, sizeof(line), "#!%s\n", depthB);
290 failed |= write_script(depthA, line, 0700);
291 char* depthArguments[] = {(char*)"wrong-shebang-depth-zero",
292 (char*)program,
293 depthD,
294 depthC,
295 depthB,
296 depthA,
297 (char*)"depth-tail",
298 0};
299 if (!failed)
300 failed |= !exec_succeeds(depthA, depthArguments);
301
302 snprintf(line, sizeof(line), "#!%s\t--exec-shebang-unexpected-child\n", program);
303 failed |= write_script(depthE, line, 0700);
304 snprintf(line, sizeof(line), "#!%s\n", depthE);
305 failed |= write_script(depthD, line, 0700);
306 if (!failed)
307 failed |= !exec_returns_errno(depthA, ELOOP);
308
309 char truncatedContents[320];
310 truncatedContents[0] = '#';
311 truncatedContents[1] = '!';
312 memset(truncatedContents + 2, 'a', sizeof(truncatedContents) - 3);
313 truncatedContents[sizeof(truncatedContents) - 1] = '\n';
314 failed |= write_fixture(truncated, truncatedContents, sizeof(truncatedContents), 0700) ||
315 !exec_returns_errno(truncated, ENOEXEC);
316 char unterminatedContents[256];
317 unterminatedContents[0] = '#';
318 unterminatedContents[1] = '!';
319 memset(unterminatedContents + 2, 'a', sizeof(unterminatedContents) - 2);
320 failed |= write_fixture(unterminated, unterminatedContents, sizeof(unterminatedContents), 0700) ||
321 !exec_returns_errno(unterminated, ENOEXEC);
322 failed |= write_script(malformed, "#!\t \n", 0700) || !exec_returns_errno(malformed, ENOEXEC);
323
324 for (size_t i = 0; i < fixtureCount; ++i) {
325 if (unlink(fixtures[i]) && errno != ENOENT)
326 failed = 1;
327 }
328
329 if (failed)
330 fail();
331 puts("OK");
332 fflush(stdout);
333}