Cygwin: posix_spawn: add Cygwin-specific code fixing process synchronisation

Newlib's posix_spawn has been taken from FreeBSD.  The code relies on
BSD-specific behaviour of vfork, namely the fact that vfork blocks
the parent until the child exits or calls execve as well as the fact
that the child shares parent memory in non-COW mode.

This behaviour can't be emulated by Cygwin.  Cygwin's vfork is
equivalent to fork.  This is POSIX-compliant, but it's lacking BSD's
vfork ingrained synchronization of the parent to wait for the child
calling execve, or the chance to just write a variable and the parent
will see the result.

So this requires a Cygwin-specific solution.  The core function of
posix_spawn, called do_posix_spawn is now implemented twice, once using
the BSD method, and once for Cygwin using Windows synchronization under
the hood waiting for the child to call execve and signalling errors
upstream.  The Windows specifics are hidden inside Cygwin, so newlib
only calls internal Cygwin functions.

Signed-off-by: Corinna Vinschen <corinna@vinschen.de>
This commit is contained in:
Corinna Vinschen 2020-08-01 21:35:58 +02:00
parent c222c1b294
commit 3fbfcd11fb
4 changed files with 206 additions and 8 deletions

View File

@ -254,6 +254,69 @@ process_file_actions(const posix_spawn_file_actions_t fa)
return (0); return (0);
} }
#ifdef __CYGWIN__
/* Cygwin's vfork does not follow BSD vfork semantics. Rather it's equivalent
to fork. While that's POSIX compliant, the below FreeBSD implementation
relying on BSD vfork semantics doesn't work as expected on Cygwin. The
following Cygwin-specific code handles the synchronization FreeBSD gets
for free by using vfork. */
extern int __posix_spawn_sem_create (void **semp);
extern void __posix_spawn_sem_release (void *sem, int error);
extern int __posix_spawn_sem_wait_and_close (void *sem, void *proc);
extern int __posix_spawn_fork (void **proc);
extern int __posix_spawn_execvpe (const char *path, char * const *argv,
char *const *envp, void *sem,
int use_env_path);
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)
{
int error;
void *sem, *proc;
pid_t p;
error = __posix_spawn_sem_create(&sem);
if (error)
return error;
p = __posix_spawn_fork(&proc);
switch (p) {
case -1:
return (errno);
case 0:
if (sa != NULL) {
error = process_spawnattr(*sa);
if (error) {
__posix_spawn_sem_release(sem, error);
_exit(127);
}
}
if (fa != NULL) {
error = process_file_actions(*fa);
if (error) {
__posix_spawn_sem_release(sem, error);
_exit(127);
}
}
__posix_spawn_execvpe(path, argv,
envp != NULL ? envp : *p_environ,
sem, use_env_path);
_exit(127);
default:
error = __posix_spawn_sem_wait_and_close(sem, proc);
if (error != 0)
waitpid(p, NULL, WNOHANG);
else if (pid != NULL)
*pid = p;
return (error);
}
}
#else
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,
@ -292,6 +355,7 @@ do_posix_spawn(pid_t *pid, const char *path,
return (error); return (error);
} }
} }
#endif
int int
posix_spawn (pid_t *pid, posix_spawn (pid_t *pid,

View File

@ -37,7 +37,7 @@ enum child_status
#define EXEC_MAGIC_SIZE sizeof(child_info) #define EXEC_MAGIC_SIZE sizeof(child_info)
/* Change this value if you get a message indicating that it is out-of-sync. */ /* Change this value if you get a message indicating that it is out-of-sync. */
#define CURR_CHILD_INFO_MAGIC 0xf4531879U #define CURR_CHILD_INFO_MAGIC 0xecc930b9U
#define NPROCS 256 #define NPROCS 256
@ -144,6 +144,7 @@ class child_info_spawn: public child_info
{ {
HANDLE hExeced; HANDLE hExeced;
HANDLE ev; HANDLE ev;
HANDLE sem;
pid_t cygpid; pid_t cygpid;
public: public:
cygheap_exec_info *moreinfo; cygheap_exec_info *moreinfo;
@ -159,6 +160,11 @@ public:
void *operator new (size_t, void *p) __attribute__ ((nothrow)) {return p;} void *operator new (size_t, void *p) __attribute__ ((nothrow)) {return p;}
void set (child_info_types ci, bool b) { new (this) child_info_spawn (ci, b);} void set (child_info_types ci, bool b) { new (this) child_info_spawn (ci, b);}
void __reg1 handle_spawn (); void __reg1 handle_spawn ();
void set_sem (HANDLE _sem)
{
/* Don't leak semaphore handle into exec'ed process. */
SetHandleInformation (sem = _sem, HANDLE_FLAG_INHERIT, 0);
}
bool set_saw_ctrl_c () bool set_saw_ctrl_c ()
{ {
if (!has_execed ()) if (!has_execed ())
@ -188,8 +194,8 @@ public:
bool get_parent_handle (); bool get_parent_handle ();
bool has_execed_cygwin () const { return iscygwin () && has_execed (); } bool has_execed_cygwin () const { return iscygwin () && has_execed (); }
operator HANDLE& () {return hExeced;} operator HANDLE& () {return hExeced;}
int __reg3 worker (const char *, const char *const *, const char *const [], int, int __reg3 worker (const char *, const char *const *, const char *const [],
int = -1, int = -1);; int, int = -1, int = -1);
}; };
extern child_info_spawn ch_spawn; extern child_info_spawn ch_spawn;

View File

@ -31,7 +31,7 @@ details. */
/* FIXME: Once things stabilize, bump up to a few minutes. */ /* FIXME: Once things stabilize, bump up to a few minutes. */
#define FORK_WAIT_TIMEOUT (300 * 1000) /* 300 seconds */ #define FORK_WAIT_TIMEOUT (300 * 1000) /* 300 seconds */
static int dofork (bool *with_forkables); static int dofork (void **proc, bool *with_forkables);
class frok class frok
{ {
frok (bool *forkables) frok (bool *forkables)
@ -47,7 +47,7 @@ class frok
int __stdcall parent (volatile char * volatile here); int __stdcall parent (volatile char * volatile here);
int __stdcall child (volatile char * volatile here); int __stdcall child (volatile char * volatile here);
bool error (const char *fmt, ...); bool error (const char *fmt, ...);
friend int dofork (bool *with_forkables); friend int dofork (void **proc, bool *with_forkables);
}; };
static void static void
@ -583,17 +583,36 @@ extern "C" int
fork () fork ()
{ {
bool with_forkables = false; /* do not force hardlinks on first try */ bool with_forkables = false; /* do not force hardlinks on first try */
int res = dofork (&with_forkables); int res = dofork (NULL, &with_forkables);
if (res >= 0) if (res >= 0)
return res; return res;
if (with_forkables) if (with_forkables)
return res; /* no need for second try when already enabled */ return res; /* no need for second try when already enabled */
with_forkables = true; /* enable hardlinks for second try */ with_forkables = true; /* enable hardlinks for second try */
return dofork (&with_forkables); return dofork (NULL, &with_forkables);
}
/* __posix_spawn_fork is called from newlib's posix_spawn implementation.
The original code in newlib has been taken from FreeBSD, and the core
code relies on specific, non-portable behaviour of vfork(2). Our
replacement implementation needs the forked child's HANDLE for
synchronization, so __posix_spawn_fork returns it in proc. */
extern "C" int
__posix_spawn_fork (void **proc)
{
bool with_forkables = false; /* do not force hardlinks on first try */
int res = dofork (proc, &with_forkables);
if (res >= 0)
return res;
if (with_forkables)
return res; /* no need for second try when already enabled */
with_forkables = true; /* enable hardlinks for second try */
return dofork (proc, &with_forkables);
} }
static int static int
dofork (bool *with_forkables) dofork (void **proc, bool *with_forkables)
{ {
frok grouped (with_forkables); frok grouped (with_forkables);
@ -671,6 +690,11 @@ dofork (bool *with_forkables)
set_errno (grouped.this_errno); set_errno (grouped.this_errno);
} }
else if (proc)
{
/* Return child process handle to posix_fork. */
*proc = grouped.hchild;
}
syscall_printf ("%R = fork()", res); syscall_printf ("%R = fork()", res);
return res; return res;
} }

View File

@ -252,6 +252,8 @@ struct system_call_handle
child_info_spawn NO_COPY ch_spawn; child_info_spawn NO_COPY ch_spawn;
extern "C" void __posix_spawn_sem_release (void *sem, int error);
int int
child_info_spawn::worker (const char *prog_arg, const char *const *argv, child_info_spawn::worker (const char *prog_arg, const char *const *argv,
const char *const envp[], int mode, const char *const envp[], int mode,
@ -897,6 +899,8 @@ child_info_spawn::worker (const char *prog_arg, const char *const *argv,
&& WaitForSingleObject (pi.hProcess, 0) == WAIT_TIMEOUT) && WaitForSingleObject (pi.hProcess, 0) == WAIT_TIMEOUT)
wait_for_myself (); wait_for_myself ();
} }
if (sem)
__posix_spawn_sem_release (sem, 0);
myself.exit (EXITCODE_NOSET); myself.exit (EXITCODE_NOSET);
break; break;
case _P_WAIT: case _P_WAIT:
@ -1295,3 +1299,103 @@ err:
__seterrno (); __seterrno ();
return -1; return -1;
} }
/* The following __posix_spawn_* functions are called from newlib's posix_spawn
implementation. The original code in newlib has been taken from FreeBSD,
and the core code relies on specific, non-portable behaviour of vfork(2).
Our replacement implementation uses a semaphore to synchronize parent and
child process. Note: __posix_spawn_fork in fork.cc is part of the set. */
/* Create an inheritable semaphore. Set it to 0 (== non-signalled), so the
parent can wait on the semaphore immediately. */
extern "C" int
__posix_spawn_sem_create (void **semp)
{
HANDLE sem;
OBJECT_ATTRIBUTES attr;
NTSTATUS status;
if (!semp)
return EINVAL;
InitializeObjectAttributes (&attr, NULL, OBJ_INHERIT, NULL, NULL);
status = NtCreateSemaphore (&sem, SEMAPHORE_ALL_ACCESS, &attr, 0, INT_MAX);
if (!NT_SUCCESS (status))
return geterrno_from_nt_status (status);
*semp = sem;
return 0;
}
/* Signal the semaphore. "error" should be 0 if all went fine and the
exec'd child process is up and running, a useful POSIX error code otherwise.
After releasing the semaphore, the value of the semaphore reflects
the error code + 1. Thus, after WFMO in__posix_spawn_sem_wait_and_close,
querying the value of the semaphore returns either 0 if all went well,
or a value > 0 equivalent to the POSIX error code. */
extern "C" void
__posix_spawn_sem_release (void *sem, int error)
{
ReleaseSemaphore (sem, error + 1, NULL);
}
/* Helper to check the semaphore value. */
static inline int
__posix_spawn_sem_query (void *sem)
{
SEMAPHORE_BASIC_INFORMATION sbi;
NtQuerySemaphore (sem, SemaphoreBasicInformation, &sbi, sizeof sbi, NULL);
return sbi.CurrentCount;
}
/* Called from parent to wait for fork/exec completion. We're waiting for
the semaphore as well as the child's process handle, so even if the
child crashes without signalling the semaphore, we won't wait infinitely. */
extern "C" int
__posix_spawn_sem_wait_and_close (void *sem, void *proc)
{
int ret = 0;
HANDLE w4[2] = { sem, proc };
switch (WaitForMultipleObjects (2, w4, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
ret = __posix_spawn_sem_query (sem);
break;
case WAIT_OBJECT_0 + 1:
/* If we return here due to the child process dying, the semaphore is
very likely not signalled. Check this here and return a valid error
code. */
ret = __posix_spawn_sem_query (sem);
if (ret == 0)
ret = ECHILD;
break;
default:
ret = geterrno_from_win_error ();
break;
}
CloseHandle (sem);
return ret;
}
/* Replacement for execve/execvpe, called from forked child in newlib's
posix_spawn. The relevant difference is the additional semaphore
so the worker method (which is not supposed to return on success)
can signal the semaphore after sync'ing with the exec'd child. */
extern "C" int
__posix_spawn_execvpe (const char *path, char * const *argv, char *const *envp,
HANDLE sem, int use_env_path)
{
path_conv buf;
static char *const empty_env[] = { NULL };
if (!envp)
envp = empty_env;
ch_spawn.set_sem (sem);
ch_spawn.worker (use_env_path ? (find_exec (path, buf, "PATH", FE_NNF) ?: "")
: path,
argv, envp,
_P_OVERLAY | (use_env_path ? _P_PATH_TYPE_EXEC : 0));
__posix_spawn_sem_release (sem, errno);
return -1;
}