mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
A process keeps its pid, its descriptors, its working directory and its mounts across an exec; what it loses is the address space and the program in it. From there the work is the same as starting fresh, so this is the same function -- run_program, told not to build a new kernel. Close-on-exec descriptors go and the rest survive with their offsets, which is how a shell hands a pipe to the command it starts. Everything that can fail is checked before the point of no return: a failed execve has to *return*, with the guest seeing ENOENT and carrying on in the program it already had, and once the host starts building a new address space there is nothing to return to. The strings are read in the kernel for the same reason -- the memory holding them is about to go away. The file is read through the guest's namespace, so a sysroot is honoured and /bin/sh means the guest's. Two bugs found by the test. F_GETFD answered for a closed descriptor: the flag vector keeps a slot after the entry is gone, and only the flags were being checked, so a program could not tell whether execve had closed what it asked to be closed. That one predates exec. And the failed-exec path killed the child instead of returning, which is what moved the checks before the commit point. QEMU cannot be the oracle here, which is worth stating plainly: qemu-aarch64 re-execs only itself, for the program it was given, so a guest that execs another aarch64 binary fails under QEMU and works under gex. The test therefore checks a written-down expectation of Linux's behaviour, and the harness grew an "expect" beside "compare" to say so. I confirmed it fails when the expectation is wrong. Suite is 44.
57 lines
1.8 KiB
C
57 lines
1.8 KiB
C
/* execve: argv, envp, close-on-exec, and a failed exec. */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/wait.h>
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc > 1 && !strcmp(argv[1], "child")) {
|
|
printf("argc=%d", argc);
|
|
for (int i = 1; i < argc; i++) printf(" [%s]", argv[i]);
|
|
printf("\n");
|
|
printf("env GEXA=%s GEXB=%s\n", getenv("GEXA") ?: "(unset)", getenv("GEXB") ?: "(unset)");
|
|
/* fd 7 was marked close-on-exec; fd 8 was not. */
|
|
printf("fd7 %s fd8 %s\n",
|
|
fcntl(7, F_GETFD) < 0 ? "closed" : "open",
|
|
fcntl(8, F_GETFD) < 0 ? "closed" : "open");
|
|
char buf[32] = {0};
|
|
ssize_t n = read(8, buf, sizeof buf - 1);
|
|
printf("inherited fd reads %zd bytes: %s\n", n, n > 0 ? buf : "");
|
|
return 42;
|
|
}
|
|
|
|
FILE *f = fopen("/tmp/gex-exec-data", "w");
|
|
fputs("through the pipe", f);
|
|
fclose(f);
|
|
|
|
int a = open("/tmp/gex-exec-data", O_RDONLY | O_CLOEXEC);
|
|
int b = open("/tmp/gex-exec-data", O_RDONLY);
|
|
dup2(a, 7); dup2(b, 8);
|
|
fcntl(7, F_SETFD, FD_CLOEXEC);
|
|
|
|
pid_t p = fork();
|
|
if (p == 0) {
|
|
/* argv[0], so the test does not depend on the working directory. */
|
|
char *args[] = {argv[0], "child", "two", "three", NULL};
|
|
char *env[] = {"GEXA=one", "GEXB=two", NULL};
|
|
execve(argv[0], args, env);
|
|
_exit(127);
|
|
}
|
|
int st = 0; waitpid(p, &st, 0);
|
|
printf("child status %d\n", WEXITSTATUS(st));
|
|
|
|
/* A failed exec must return, not replace the process. */
|
|
p = fork();
|
|
if (p == 0) {
|
|
char *args[] = {"/nonexistent", NULL};
|
|
execve("/nonexistent", args, NULL);
|
|
_exit(99);
|
|
}
|
|
waitpid(p, &st, 0);
|
|
printf("failed exec status %d\n", WEXITSTATUS(st));
|
|
printf("done\n");
|
|
return 0;
|
|
}
|