Commit ed78b9e2 authored by Christian Persch's avatar Christian Persch

spawn: Async-signal-safety fixes

Ported from glib#2140
with some adjustments.

Fixes: #263
parent 45de5443
......@@ -51,6 +51,9 @@ struct linux_dirent64
char d_name[]; /* Filename (null-terminated) */
};
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
static int
filename_to_fd (const char *p)
{
......@@ -64,7 +67,7 @@ filename_to_fd (const char *p)
while ((c = *p++) != '\0')
{
if (!g_ascii_isdigit (c))
if (c < '0' || c > '9')
return -1;
c -= '0';
......@@ -80,6 +83,56 @@ filename_to_fd (const char *p)
#endif /* __linux__ */
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
static int
getrlimit_NOFILE_max(void)
{
#ifdef HAVE_SYS_RESOURCE_H
struct rlimit rlim;
#ifdef __linux__
if (prlimit(0 /* this PID */, RLIMIT_NOFILE, nullptr, &rlim) == 0 &&
rlim.rlim_max != RLIM_INFINITY)
return rlim.rlim_max;
/* fallback */
#endif /* __linux__ */
#ifdef __GLIBC__
/* Use getrlimit() function provided by the system if it is known to be
* async-signal safe.
*
* According to the glibc manual, getrlimit is AS-safe.
*/
if (getrlimit(RLIMIT_NOFILE, &rlim) == 0 &&
rlim.rlim_max != RLIM_INFINITY)
return rlim.rlim_max;
/* fallback */
#endif
#endif /* HAVE_SYS_RESOURCE_H */
#if defined(__FreeBSD__) || defined(__OpenBSD__)
/* Use sysconf() function provided by the system if it is known to be
* async-signal safe.
*/
auto const r = sysconf(_SC_OPEN_MAX);
if (r != -1)
return r;
/* fallback */
#endif
/* Hardcoded fallback: the default process hard limit in Linux as of 2020 */
return 4096;
}
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
int
fdwalk(int (*cb)(void *data, int fd),
void *data)
......@@ -88,14 +141,9 @@ fdwalk(int (*cb)(void *data, int fd),
* may be slow on non-Linux operating systems, especially on systems allowing
* very high number of open file descriptors.
*/
int open_max;
int fd;
int res = 0;
#ifdef HAVE_SYS_RESOURCE_H
struct rlimit rl;
#endif
#ifdef __linux__
/* Avoid use of opendir/closedir since these are not async-signal-safe. */
int dir_fd = open ("/proc/self/fd", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
......@@ -129,14 +177,7 @@ fdwalk(int (*cb)(void *data, int fd),
#endif
#ifdef HAVE_SYS_RESOURCE_H
if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY)
open_max = rl.rlim_max;
else
#endif
open_max = sysconf (_SC_OPEN_MAX);
auto const open_max = getrlimit_NOFILE_max();
for (fd = 0; fd < open_max; fd++)
if ((res = cb (data, fd)) != 0)
break;
......
......@@ -278,8 +278,28 @@ SpawnContext::prepare_environ()
m_envv = vte::glib::take_strv(merge_environ(m_envv.release(), m_cwd.get(), inherit_environ()));
}
char const*
SpawnContext::search_path() const noexcept
{
auto const path = m_search_path ? g_environ_getenv(environ(), "PATH") : nullptr;
return path ? : "/bin:/usr/bin:.";
}
size_t
SpawnContext::workbuf_size() const noexcept
{
auto const path = search_path();
return std::max(path ? strlen(path) + strlen(arg0()) + 2 /* leading '/' plus NUL terminator */ : 0,
(g_strv_length(argv()) + 2) * sizeof(char*));
}
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
SpawnContext::ExecError
SpawnContext::exec(vte::libc::FD& child_report_error_pipe_write) noexcept
SpawnContext::exec(vte::libc::FD& child_report_error_pipe_write,
void* workbuf,
size_t workbufsize) noexcept
{
/* NOTE! This function must not rely on smart pointers to
* release their object, since the destructors are NOT run
......@@ -449,7 +469,8 @@ SpawnContext::exec(vte::libc::FD& child_report_error_pipe_write) noexcept
argv(),
environ(),
search_path(),
search_path_from_envp());
workbuf,
workbufsize);
/* If we get here, exec failed */
return ExecError::EXEC;
......@@ -508,6 +529,18 @@ SpawnOperation::prepare(vte::glib::Error& error)
error))
return false;
/* allocate workbuf for SpawnContext::Exec() */
auto const workbufsize = context().workbuf_size();
auto workbuf = vte::glib::take_free_ptr(g_try_malloc(workbufsize));
if (!workbuf) {
auto errsv = vte::libc::ErrnoSaver{};
error.set(G_IO_ERROR,
g_io_error_from_errno(errsv),
"Failed to allocate workbuf: %s",
g_strerror(errsv));
return false;
}
/* Need to add the write end of the pipe to the FD map, so
* that the FD re-arranging code knows it needs to preserve
* the FD and not dup2 over it.
......@@ -530,7 +563,11 @@ SpawnOperation::prepare(vte::glib::Error& error)
child_report_error_pipe_read.reset();
auto const err = context().exec(child_report_error_pipe_write);
auto const err = context().exec(child_report_error_pipe_write,
workbuf.get(), workbufsize);
/* Manually free the workbuf */
g_free(workbuf.release());
/* If we get here, exec failed. Write the error to the pipe and exit. */
_vte_write_err(child_report_error_pipe_write.get(), int(err));
......
......@@ -59,7 +59,6 @@ private:
bool m_inherit_environ{true};
bool m_systemd_scope{true};
bool m_require_systemd_scope{false};
bool m_search_path_from_envp{false};
bool m_search_path{false};
public:
......@@ -149,7 +148,6 @@ public:
void set_no_systemd_scope() noexcept { m_systemd_scope = false; }
void set_require_systemd_scope() noexcept { m_require_systemd_scope = true; }
void set_search_path() noexcept { m_search_path = true; }
void set_search_path_from_envp() noexcept { m_search_path_from_envp = true; }
auto arg0() const noexcept { return m_arg0.get(); }
auto argv() const noexcept { return m_argv.get(); }
......@@ -163,8 +161,9 @@ public:
constexpr auto inherit_environ() const noexcept { return m_inherit_environ; }
constexpr auto systemd_scope() const noexcept { return m_systemd_scope; }
constexpr auto require_systemd_scope() const noexcept { return m_require_systemd_scope; }
constexpr auto search_path() const noexcept { return m_search_path; }
constexpr auto search_path_from_envp() const noexcept { return m_search_path_from_envp; }
char const* search_path() const noexcept;
size_t workbuf_size() const noexcept;
void prepare_environ();
......@@ -181,7 +180,9 @@ public:
UNSET_CLOEXEC,
};
ExecError exec(vte::libc::FD& child_report_error_pipe_write) noexcept;
ExecError exec(vte::libc::FD& child_report_error_pipe_write,
void* workbuf,
size_t workbufsize) noexcept;
}; // class SpawnContext
......
......@@ -624,9 +624,8 @@ spawn_context_from_args(VtePty* pty,
context.set_fallback_cwd(g_get_home_dir());
context.set_child_setup(child_setup, child_setup_data, child_setup_data_destroy);
if (spawn_flags & G_SPAWN_SEARCH_PATH_FROM_ENVP)
context.set_search_path_from_envp();
if (spawn_flags & G_SPAWN_SEARCH_PATH)
if ((spawn_flags & G_SPAWN_SEARCH_PATH_FROM_ENVP) ||
(spawn_flags & G_SPAWN_SEARCH_PATH))
context.set_search_path();
if (spawn_flags & G_SPAWN_FILE_AND_ARGV_ZERO)
......
......@@ -40,14 +40,19 @@
#include "missing.hh"
static gssize
write_all (int fd, gconstpointer vbuf, gsize to_write)
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
static ssize_t
write_all (int fd,
void const* vbuf,
size_t to_write) noexcept
{
char *buf = (char *) vbuf;
while (to_write > 0)
{
gssize count = write (fd, buf, to_write);
auto count = write(fd, buf, to_write);
if (count < 0)
{
if (errno != EINTR)
......@@ -63,9 +68,12 @@ write_all (int fd, gconstpointer vbuf, gsize to_write)
return TRUE;
}
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
void
_vte_write_err (int fd,
int msg)
int msg) noexcept
{
int data[2] = {msg, errno};
......@@ -74,46 +82,57 @@ _vte_write_err (int fd,
/* Based on execvp from GNU C Library */
static void
script_execute (const char *file,
char **argv,
char **envp)
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
/* Returns false if failing before execv(e), or true if failing after it. */
static bool
script_execute (char const* file,
char** argv,
char** envp,
void* workbuf,
size_t workbufsize) noexcept
{
/* Count the arguments. */
int argc = 0;
while (argv[argc])
++argc;
/* Construct an argument list for the shell. */
{
char **new_argv;
new_argv = g_new0 (char*, argc + 2); /* /bin/sh and NULL */
auto argv_buffer = reinterpret_cast<char**>(workbuf);
auto argv_buffer_len = workbufsize / sizeof(char*);
/* Construct an argument list for the shell. */
if (size_t(argc + 2) > argv_buffer_len) {
errno = ENOMEM;
return false;
}
new_argv[0] = (char *) "/bin/sh";
new_argv[1] = (char *) file;
while (argc > 0)
{
new_argv[argc + 1] = argv[argc];
--argc;
}
argv_buffer[0] = (char *) "/bin/sh";
argv_buffer[1] = (char *) file;
while (argc > 0)
{
argv_buffer[argc + 1] = argv[argc];
--argc;
}
/* Execute the shell. */
if (envp)
execve (new_argv[0], new_argv, envp);
else
execv (new_argv[0], new_argv);
/* Execute the shell. */
if (envp)
execve (argv_buffer[0], argv_buffer, envp);
else
execv (argv_buffer[0], argv_buffer);
g_free (new_argv);
}
return true;
}
/* This function is called between fork and execve/_exit and so must be
* async-signal-safe; see man:signal-safety(7).
*/
int
_vte_execute (const char *file,
char **argv,
char **envp,
bool search_path,
bool search_path_from_envp)
char const* search_path,
void* workbuf,
size_t workbufsize) noexcept
{
if (*file == '\0')
{
......@@ -122,7 +141,7 @@ _vte_execute (const char *file,
return -1;
}
if (!(search_path || search_path_from_envp) || strchr (file, '/') != NULL)
if (!search_path || strchr (file, '/') != nullptr)
{
/* Don't search when it contains a slash. */
if (envp)
......@@ -131,40 +150,23 @@ _vte_execute (const char *file,
execv (file, argv);
if (errno == ENOEXEC)
script_execute (file, argv, envp);
script_execute(file, argv, envp, workbuf, workbufsize);
}
else
{
gboolean got_eacces = 0;
const char *path, *p;
char *name, *freeme;
gsize len;
gsize pathlen;
path = NULL;
if (search_path_from_envp)
path = g_environ_getenv (envp, "PATH");
if (search_path && path == NULL)
path = g_getenv ("PATH");
if (path == NULL)
{
/* There is no 'PATH' in the environment. The default
* search path in libc is the current directory followed by
* the path 'confstr' returns for '_CS_PATH'.
*/
auto got_eacces = false;
char const* path = search_path;
char const* p;
char* name = reinterpret_cast<char*>(workbuf);
/* In GLib we put . last, for security, and don't use the
* unportable confstr(); UNIX98 does not actually specify
* what to search if PATH is unset. POSIX may, dunno.
*/
path = "/bin:/usr/bin:.";
}
auto const len = strlen(file) + 1;
auto const pathlen = strlen(path);
len = strlen (file) + 1;
pathlen = strlen (path);
freeme = name = (char*)g_malloc (pathlen + len + 1);
if (workbufsize < pathlen + len + 1)
{
errno = ENOMEM;
return -1;
}
/* Copy the file name at the top, including '\0' */
memcpy (name + pathlen + 1, file, len);
......@@ -186,7 +188,7 @@ _vte_execute (const char *file,
*/
startp = name + 1;
else
startp = (char*)memcpy (name - (p - path), path, p - path);
startp = reinterpret_cast<char*>(memcpy (name - (p - path), path, p - path));
/* Try to execute this name. If it works, execv will not return. */
if (envp)
......@@ -194,8 +196,9 @@ _vte_execute (const char *file,
else
execv (startp, argv);
if (errno == ENOEXEC)
script_execute (startp, argv, envp);
if (errno == ENOEXEC &&
!script_execute(startp, argv, envp, workbuf, workbufsize))
return -1;
switch (errno)
{
......@@ -206,8 +209,7 @@ _vte_execute (const char *file,
*/
got_eacces = TRUE;
/* FALL THRU */
[[fallthrough]];
case ENOENT:
#ifdef ESTALE
case ESTALE:
......@@ -234,7 +236,6 @@ _vte_execute (const char *file,
* something went wrong executing it; return the error to our
* caller.
*/
g_free (freeme);
return -1;
}
}
......@@ -246,8 +247,6 @@ _vte_execute (const char *file,
* error.
*/
errno = EACCES;
g_free (freeme);
}
/* Return the error from the last attempt (probably ENOENT). */
......
......@@ -25,8 +25,9 @@
int _vte_execute(char const* file,
char** argv,
char** envp,
bool search_path,
bool search_path_from_envp);
char const* path_env,
void* workbuf,
size_t workbufsize) noexcept;
void _vte_write_err (int fd,
int msg);
int msg) noexcept;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment