Files
velzie f462c34ea2 execve
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.
2026-08-05 10:02:40 -04:00

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;
}