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:
parent
079c5b9ed8
commit
c34a5f16fa
Notes:
svn2git
2020-12-20 02:59:44 +00:00
svn path=/head/; revision=352712
@ -194,43 +194,115 @@ process_file_actions(const posix_spawn_file_actions_t fa)
|
|||||||
return (0);
|
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
|
static int
|
||||||
do_posix_spawn(pid_t *pid, const char *path,
|
do_posix_spawn(pid_t *pid, const char *path,
|
||||||
const posix_spawn_file_actions_t *fa,
|
const posix_spawn_file_actions_t *fa,
|
||||||
const posix_spawnattr_t *sa,
|
const posix_spawnattr_t *sa,
|
||||||
char * const argv[], char * const envp[], int use_env_path)
|
char * const argv[], char * const envp[], int use_env_path)
|
||||||
{
|
{
|
||||||
|
struct posix_spawn_args psa;
|
||||||
pid_t p;
|
pid_t p;
|
||||||
volatile int error = 0;
|
#ifdef _RFORK_THREAD_STACK_SIZE
|
||||||
|
char *stack;
|
||||||
|
|
||||||
p = vfork();
|
stack = malloc(_RFORK_THREAD_STACK_SIZE);
|
||||||
switch (p) {
|
if (stack == NULL)
|
||||||
case -1:
|
return (ENOMEM);
|
||||||
return (errno);
|
#endif
|
||||||
case 0:
|
psa.path = path;
|
||||||
if (sa != NULL) {
|
psa.fa = fa;
|
||||||
error = process_spawnattr(*sa);
|
psa.sa = sa;
|
||||||
if (error)
|
psa.argv = argv;
|
||||||
_exit(127);
|
psa.envp = envp;
|
||||||
}
|
psa.use_env_path = use_env_path;
|
||||||
if (fa != NULL) {
|
psa.error = 0;
|
||||||
error = process_file_actions(*fa);
|
|
||||||
if (error)
|
/*
|
||||||
_exit(127);
|
* Passing RFSPAWN to rfork(2) gives us effectively a vfork that drops
|
||||||
}
|
* non-ignored signal handlers. We'll fall back to the slightly less
|
||||||
if (use_env_path)
|
* ideal vfork(2) if we get an EINVAL from rfork -- this should only
|
||||||
_execvpe(path, argv, envp != NULL ? envp : environ);
|
* happen with newer libc on older kernel that doesn't accept
|
||||||
else
|
* RFSPAWN.
|
||||||
_execve(path, argv, envp != NULL ? envp : environ);
|
*/
|
||||||
error = errno;
|
#ifdef _RFORK_THREAD_STACK_SIZE
|
||||||
_exit(127);
|
/*
|
||||||
default:
|
* x86 stores the return address on the stack, so rfork(2) cannot work
|
||||||
if (error != 0)
|
* as-is because the child would clobber the return address om the
|
||||||
_waitpid(p, NULL, WNOHANG);
|
* parent. Because of this, we must use rfork_thread instead while
|
||||||
else if (pid != NULL)
|
* almost every other arch stores the return address in a register.
|
||||||
*pid = p;
|
*/
|
||||||
return (error);
|
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
|
int
|
||||||
|
@ -113,6 +113,9 @@ is passed,
|
|||||||
will use
|
will use
|
||||||
.Xr vfork 2
|
.Xr vfork 2
|
||||||
semantics but reset all signal actions in the child to default.
|
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
|
.Pp
|
||||||
If
|
If
|
||||||
.Dv RFPROC
|
.Dv RFPROC
|
||||||
|
Loading…
Reference in New Issue
Block a user