posix_spawn(3): handle potential signal issues with vfork

Described in [1], signal handlers running in a vfork child have
opportunities to corrupt the parent's state. Address this by adding a new
rfork(2) flag, RFSPAWN, that has vfork(2) semantics but also resets signal
handlers in the child during creation.

x86 uses rfork_thread(3) instead of a direct rfork(2) because rfork with
RFMEM/RFSPAWN cannot work when the return address is stored on the stack --
further information about this problem is described under RFMEM in the
rfork(2) man page.

Addressing this has been identified as a prerequisite to using posix_spawn
in subprocess on FreeBSD [2].

[1] https://ewontfix.com/7/
[2] https://bugs.python.org/issue35823

Reviewed by:	jilles, kib
Differential Revision:	https://reviews.freebsd.org/D19058
This commit is contained in:
Kyle Evans 2019-09-25 19:22:03 +00:00
parent 079c5b9ed8
commit c34a5f16fa
2 changed files with 103 additions and 28 deletions

View File

@ -194,43 +194,115 @@ process_file_actions(const posix_spawn_file_actions_t fa)
return (0);
}
struct posix_spawn_args {
const char *path;
const posix_spawn_file_actions_t *fa;
const posix_spawnattr_t *sa;
char * const * argv;
char * const * envp;
int use_env_path;
int error;
};
#if defined(__i386__) || defined(__amd64__)
#define _RFORK_THREAD_STACK_SIZE 4096
#endif
static int
_posix_spawn_thr(void *data)
{
struct posix_spawn_args *psa;
char * const *envp;
psa = data;
if (psa->sa != NULL) {
psa->error = process_spawnattr(*psa->sa);
if (psa->error)
_exit(127);
}
if (psa->fa != NULL) {
psa->error = process_file_actions(*psa->fa);
if (psa->error)
_exit(127);
}
envp = psa->envp != NULL ? psa->envp : environ;
if (psa->use_env_path)
_execvpe(psa->path, psa->argv, envp);
else
_execve(psa->path, psa->argv, envp);
psa->error = errno;
/* This is called in such a way that it must not exit. */
_exit(127);
}
static int
do_posix_spawn(pid_t *pid, const char *path,
const posix_spawn_file_actions_t *fa,
const posix_spawnattr_t *sa,
char * const argv[], char * const envp[], int use_env_path)
{
struct posix_spawn_args psa;
pid_t p;
volatile int error = 0;
#ifdef _RFORK_THREAD_STACK_SIZE
char *stack;
p = vfork();
switch (p) {
case -1:
return (errno);
case 0:
if (sa != NULL) {
error = process_spawnattr(*sa);
if (error)
_exit(127);
}
if (fa != NULL) {
error = process_file_actions(*fa);
if (error)
_exit(127);
}
if (use_env_path)
_execvpe(path, argv, envp != NULL ? envp : environ);
else
_execve(path, argv, envp != NULL ? envp : environ);
error = errno;
_exit(127);
default:
if (error != 0)
_waitpid(p, NULL, WNOHANG);
else if (pid != NULL)
*pid = p;
return (error);
stack = malloc(_RFORK_THREAD_STACK_SIZE);
if (stack == NULL)
return (ENOMEM);
#endif
psa.path = path;
psa.fa = fa;
psa.sa = sa;
psa.argv = argv;
psa.envp = envp;
psa.use_env_path = use_env_path;
psa.error = 0;
/*
* Passing RFSPAWN to rfork(2) gives us effectively a vfork that drops
* non-ignored signal handlers. We'll fall back to the slightly less
* ideal vfork(2) if we get an EINVAL from rfork -- this should only
* happen with newer libc on older kernel that doesn't accept
* RFSPAWN.
*/
#ifdef _RFORK_THREAD_STACK_SIZE
/*
* x86 stores the return address on the stack, so rfork(2) cannot work
* as-is because the child would clobber the return address om the
* parent. Because of this, we must use rfork_thread instead while
* almost every other arch stores the return address in a register.
*/
p = rfork_thread(RFSPAWN, stack + _RFORK_THREAD_STACK_SIZE,
_posix_spawn_thr, &psa);
free(stack);
#else
p = rfork(RFSPAWN);
if (p == 0)
/* _posix_spawn_thr does not return */
_posix_spawn_thr(&psa);
#endif
/*
* The above block should leave us in a state where we've either
* succeeded and we're ready to process the results, or we need to
* fallback to vfork() if the kernel didn't like RFSPAWN.
*/
if (p == -1 && errno == EINVAL) {
p = vfork();
if (p == 0)
/* _posix_spawn_thr does not return */
_posix_spawn_thr(&psa);
}
if (p == -1)
return (errno);
if (psa.error != 0)
/* Failed; ready to reap */
_waitpid(p, NULL, WNOHANG);
else if (pid != NULL)
/* exec succeeded */
*pid = p;
return (psa.error);
}
int

View File

@ -113,6 +113,9 @@ is passed,
will use
.Xr vfork 2
semantics but reset all signal actions in the child to default.
This flag is used by the
.Xr posix_spawn 3
implementation in libc.
.Pp
If
.Dv RFPROC